openzca 0.1.42 → 0.1.43

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +236 -96
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,6 +36,9 @@ openzca msg send USER_ID "Hello"
36
36
  # Send to a group
37
37
  openzca msg send GROUP_ID "Hello team" --group
38
38
 
39
+ # Mention a group member by display name or username
40
+ openzca msg send GROUP_ID "Hi @Alice Nguyen" --group
41
+
39
42
  # Listen for incoming messages
40
43
  openzca listen
41
44
  ```
@@ -80,6 +83,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
80
83
 
81
84
  Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
82
85
  Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
86
+ Group text sends via `openzca msg send --group` resolve unique `@Name` mentions against the current group member list using display names and usernames. Mention offsets are computed after formatting markers are parsed, so messages like `**@Alice Nguyen** hello` work. If multiple members share the same label, the command fails instead of guessing.
83
87
 
84
88
  ### Debug Logging
85
89
 
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  Gender,
16
16
  Reactions,
17
17
  ReviewPendingMemberRequestStatus,
18
- ThreadType
18
+ ThreadType as ThreadType2
19
19
  } from "zca-js";
20
20
 
21
21
  // src/lib/store.ts
@@ -578,6 +578,113 @@ async function assertFilesExist(files) {
578
578
  }
579
579
  }
580
580
 
581
+ // src/lib/text-send.ts
582
+ import { ThreadType } from "zca-js";
583
+
584
+ // src/lib/group-mentions.ts
585
+ var ALLOWED_START_BOUNDARY_CHARS = /* @__PURE__ */ new Set(["(", "[", "{", "<", '"', ",", ";", ":"]);
586
+ var ALLOWED_END_BOUNDARY_CHARS = /* @__PURE__ */ new Set([",", ";", ":", "!", "?", ")", "]", "}", ">", '"']);
587
+ function resolveOutboundGroupMentions(text, members) {
588
+ if (!text.includes("@") || members.length === 0) {
589
+ return [];
590
+ }
591
+ const candidates = buildCandidates(members);
592
+ if (candidates.length === 0) {
593
+ return [];
594
+ }
595
+ const lowerText = text.toLowerCase();
596
+ const mentions = [];
597
+ for (let index = 0; index < text.length; index += 1) {
598
+ if (text[index] !== "@") continue;
599
+ if (!isMentionStartBoundary(text, index)) continue;
600
+ const match = resolveMentionAtIndex(text, lowerText, index, candidates);
601
+ if (!match) continue;
602
+ mentions.push({
603
+ pos: index,
604
+ len: match.label.length + 1,
605
+ uid: match.userId
606
+ });
607
+ index += match.label.length;
608
+ }
609
+ return mentions;
610
+ }
611
+ function hasPotentialOutboundGroupMention(text) {
612
+ for (let index = 0; index < text.length; index += 1) {
613
+ if (text[index] !== "@") continue;
614
+ if (!isMentionStartBoundary(text, index)) continue;
615
+ const next = text[index + 1];
616
+ if (next && !/\s/u.test(next)) {
617
+ return true;
618
+ }
619
+ }
620
+ return false;
621
+ }
622
+ function resolveMentionAtIndex(text, lowerText, atIndex, candidates) {
623
+ const start = atIndex + 1;
624
+ const matches = candidates.filter((candidate) => {
625
+ const end = start + candidate.label.length;
626
+ return lowerText.startsWith(candidate.normalizedLabel, start) && isMentionBoundary(text, start, end);
627
+ });
628
+ if (matches.length === 0) {
629
+ return null;
630
+ }
631
+ const longestLength = matches.reduce((max, candidate) => Math.max(max, candidate.label.length), 0);
632
+ const longestMatches = matches.filter((candidate) => candidate.label.length === longestLength);
633
+ const matchedUserIds = [...new Set(longestMatches.map((candidate) => candidate.userId))];
634
+ if (matchedUserIds.length !== 1) {
635
+ const label = text.slice(atIndex, start + longestLength);
636
+ throw new Error(`Ambiguous mention "${label}": matched multiple group members.`);
637
+ }
638
+ return longestMatches[0];
639
+ }
640
+ function buildCandidates(members) {
641
+ const candidates = /* @__PURE__ */ new Map();
642
+ for (const member of members) {
643
+ const userId = member.userId.trim();
644
+ if (!userId) continue;
645
+ for (const rawLabel of [member.displayName, member.zaloName]) {
646
+ const label = rawLabel?.trim();
647
+ if (!label) continue;
648
+ const normalizedLabel = label.toLowerCase();
649
+ const key = `${userId}\0${normalizedLabel}`;
650
+ if (candidates.has(key)) continue;
651
+ candidates.set(key, {
652
+ label,
653
+ normalizedLabel,
654
+ userId
655
+ });
656
+ }
657
+ }
658
+ return [...candidates.values()].sort((left, right) => right.label.length - left.label.length);
659
+ }
660
+ function isMentionBoundary(text, start, end) {
661
+ if (end > text.length) {
662
+ return false;
663
+ }
664
+ const next = text[end];
665
+ if (!next) {
666
+ return true;
667
+ }
668
+ if (/\s/u.test(next)) {
669
+ return true;
670
+ }
671
+ if (ALLOWED_END_BOUNDARY_CHARS.has(next)) {
672
+ return true;
673
+ }
674
+ if (next === ".") {
675
+ const following = text[end + 1];
676
+ return !following || /\s/u.test(following) || ALLOWED_END_BOUNDARY_CHARS.has(following);
677
+ }
678
+ return false;
679
+ }
680
+ function isMentionStartBoundary(text, atIndex) {
681
+ if (atIndex === 0) {
682
+ return true;
683
+ }
684
+ const previous = text[atIndex - 1];
685
+ return /\s/u.test(previous) || ALLOWED_START_BOUNDARY_CHARS.has(previous);
686
+ }
687
+
581
688
  // src/lib/text-styles.ts
582
689
  import { TextStyle } from "zca-js";
583
690
  var TAG_STYLE_MAP = {
@@ -826,6 +933,35 @@ function normalizeCodeBlockLeadingWhitespace(line) {
826
933
  );
827
934
  }
828
935
 
936
+ // src/lib/text-send.ts
937
+ async function buildTextSendPayload(params) {
938
+ if (params.raw) {
939
+ const mentions2 = await resolveGroupMentionsIfNeeded(params, params.message);
940
+ return mentions2 ? { msg: params.message, mentions: mentions2 } : params.message;
941
+ }
942
+ const { text, styles } = parseTextStyles(params.message);
943
+ const mentions = await resolveGroupMentionsIfNeeded(params, text);
944
+ return {
945
+ msg: text,
946
+ styles: styles.length > 0 ? styles : void 0,
947
+ mentions
948
+ };
949
+ }
950
+ async function resolveGroupMentionsIfNeeded(params, text) {
951
+ if (params.threadType !== ThreadType.Group) {
952
+ return void 0;
953
+ }
954
+ if (!hasPotentialOutboundGroupMention(text)) {
955
+ return void 0;
956
+ }
957
+ if (!params.listGroupMembers) {
958
+ return void 0;
959
+ }
960
+ const members = await params.listGroupMembers(params.threadId);
961
+ const mentions = resolveOutboundGroupMentions(text, members);
962
+ return mentions.length > 0 ? mentions : void 0;
963
+ }
964
+
829
965
  // src/cli.ts
830
966
  var require2 = createRequire(import.meta.url);
831
967
  var { version: PKG_VERSION } = require2("../package.json");
@@ -950,7 +1086,7 @@ function output(value, asJson = false) {
950
1086
  console.log(String(value));
951
1087
  }
952
1088
  function asThreadType(groupFlag) {
953
- return groupFlag ? ThreadType.Group : ThreadType.User;
1089
+ return groupFlag ? ThreadType2.Group : ThreadType2.User;
954
1090
  }
955
1091
  function parseBooleanFromEnv(name, fallback) {
956
1092
  const raw = process.env[name]?.trim();
@@ -1160,7 +1296,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
1160
1296
  fail(parsed.requestId, "Invalid upload payload.");
1161
1297
  return;
1162
1298
  }
1163
- const threadType = parsed.threadType === "group" ? ThreadType.Group : ThreadType.User;
1299
+ const threadType = parsed.threadType === "group" ? ThreadType2.Group : ThreadType2.User;
1164
1300
  const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
1165
1301
  writeDebugLine(
1166
1302
  "listen.ipc.upload.start",
@@ -1317,7 +1453,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
1317
1453
  {
1318
1454
  profile,
1319
1455
  threadId,
1320
- threadType: threadType === ThreadType.Group ? "group" : "user",
1456
+ threadType: threadType === ThreadType2.Group ? "group" : "user",
1321
1457
  attachmentCount: attachments.length,
1322
1458
  socketPath,
1323
1459
  requestId,
@@ -1361,7 +1497,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
1361
1497
  requestId,
1362
1498
  profile,
1363
1499
  threadId,
1364
- threadType: threadType === ThreadType.Group ? "group" : "user",
1500
+ threadType: threadType === ThreadType2.Group ? "group" : "user",
1365
1501
  attachments
1366
1502
  };
1367
1503
  socket.write(`${JSON.stringify(payload)}
@@ -1423,21 +1559,21 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
1423
1559
  }
1424
1560
  async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
1425
1561
  if (groupFlag) {
1426
- return { type: ThreadType.Group, reason: "explicit_group_flag" };
1562
+ return { type: ThreadType2.Group, reason: "explicit_group_flag" };
1427
1563
  }
1428
1564
  const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
1429
1565
  if (!autoDetectEnabled) {
1430
- return { type: ThreadType.User, reason: "auto_detect_disabled" };
1566
+ return { type: ThreadType2.User, reason: "auto_detect_disabled" };
1431
1567
  }
1432
1568
  try {
1433
1569
  const cache = await readCache(profile);
1434
1570
  const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
1435
1571
  if (groupIds.has(threadId)) {
1436
- return { type: ThreadType.Group, reason: "cache_group_match" };
1572
+ return { type: ThreadType2.Group, reason: "cache_group_match" };
1437
1573
  }
1438
1574
  const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
1439
1575
  if (friendIds.has(threadId)) {
1440
- return { type: ThreadType.User, reason: "cache_friend_match" };
1576
+ return { type: ThreadType2.User, reason: "cache_friend_match" };
1441
1577
  }
1442
1578
  } catch (error) {
1443
1579
  writeDebugLine(
@@ -1452,7 +1588,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
1452
1588
  }
1453
1589
  const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
1454
1590
  if (!probeEnabled) {
1455
- return { type: ThreadType.User, reason: "probe_disabled" };
1591
+ return { type: ThreadType2.User, reason: "probe_disabled" };
1456
1592
  }
1457
1593
  const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
1458
1594
  try {
@@ -1462,7 +1598,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
1462
1598
  `Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
1463
1599
  );
1464
1600
  if (groupInfo?.gridInfoMap?.[threadId]) {
1465
- return { type: ThreadType.Group, reason: "probe_group_match" };
1601
+ return { type: ThreadType2.Group, reason: "probe_group_match" };
1466
1602
  }
1467
1603
  } catch (error) {
1468
1604
  writeDebugLine(
@@ -1475,7 +1611,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
1475
1611
  command
1476
1612
  );
1477
1613
  }
1478
- return { type: ThreadType.User, reason: "default_user" };
1614
+ return { type: ThreadType2.User, reason: "default_user" };
1479
1615
  }
1480
1616
  function parseReaction(input) {
1481
1617
  const normalized = input.trim();
@@ -1565,6 +1701,64 @@ async function buildGroupsDetailed(api) {
1565
1701
  const info = await api.getGroupInfo(ids);
1566
1702
  return ids.map((id) => info.gridInfoMap?.[id]).filter((item) => Boolean(item));
1567
1703
  }
1704
+ function normalizeGroupMemberId(value) {
1705
+ if (typeof value === "number" && Number.isFinite(value)) {
1706
+ return String(Math.trunc(value));
1707
+ }
1708
+ if (typeof value !== "string") return "";
1709
+ const trimmed = value.trim();
1710
+ if (!trimmed) return "";
1711
+ return trimmed.replace(/_\d+$/, "");
1712
+ }
1713
+ async function listGroupMemberRows(api, groupId) {
1714
+ const info = await api.getGroupInfo(groupId);
1715
+ const groupInfo = info.gridInfoMap[groupId];
1716
+ if (!groupInfo) {
1717
+ throw new Error(`Group not found: ${groupId}`);
1718
+ }
1719
+ const idsFromMemberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => normalizeGroupMemberId(id)).filter(Boolean) : [];
1720
+ const memVerList = groupInfo.memVerList;
1721
+ const idsFromMemVerList = Array.isArray(memVerList) ? memVerList.map((id) => normalizeGroupMemberId(id)).filter(Boolean) : [];
1722
+ const currentMems = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
1723
+ const currentMemberMap = /* @__PURE__ */ new Map();
1724
+ for (const member of currentMems) {
1725
+ const userId = normalizeGroupMemberId(member.id);
1726
+ if (!userId) continue;
1727
+ currentMemberMap.set(userId, {
1728
+ displayName: member.dName?.trim() || member.zaloName?.trim() || "",
1729
+ zaloName: member.zaloName?.trim() || ""
1730
+ });
1731
+ }
1732
+ const ids = Array.from(
1733
+ /* @__PURE__ */ new Set([
1734
+ ...idsFromMemberIds,
1735
+ ...idsFromMemVerList,
1736
+ ...Array.from(currentMemberMap.keys())
1737
+ ])
1738
+ );
1739
+ const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
1740
+ const rawProfileMap = profiles.profiles;
1741
+ const profileMap = /* @__PURE__ */ new Map();
1742
+ for (const [key, profile] of Object.entries(rawProfileMap)) {
1743
+ if (!profile) continue;
1744
+ const normalizedKey = normalizeGroupMemberId(key);
1745
+ if (normalizedKey && !profileMap.has(normalizedKey)) {
1746
+ profileMap.set(normalizedKey, profile);
1747
+ }
1748
+ const profileId = normalizeGroupMemberId(profile.id);
1749
+ if (profileId && !profileMap.has(profileId)) {
1750
+ profileMap.set(profileId, profile);
1751
+ }
1752
+ }
1753
+ return ids.map((id) => ({
1754
+ userId: id,
1755
+ displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
1756
+ zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
1757
+ }));
1758
+ }
1759
+ async function listGroupMentionMembers(api, threadId) {
1760
+ return await listGroupMemberRows(api, threadId);
1761
+ }
1568
1762
  async function refreshCacheForProfile(profile, api) {
1569
1763
  const [friends, groups] = await Promise.all([
1570
1764
  api.getAllFriends(),
@@ -1882,7 +2076,7 @@ function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
1882
2076
  const threadIdRaw = String(raw.idTo ?? "").trim();
1883
2077
  normalized.push({
1884
2078
  threadId: threadIdRaw || fallbackThreadId,
1885
- type: ThreadType.Group,
2079
+ type: ThreadType2.Group,
1886
2080
  data: {
1887
2081
  actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
1888
2082
  msgId: String(raw.msgId ?? ""),
@@ -1975,7 +2169,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
1975
2169
  requestedCursors.add(cursor);
1976
2170
  }
1977
2171
  pagesRequested += 1;
1978
- api.listener.requestOldMessages(ThreadType.Group, cursor || null);
2172
+ api.listener.requestOldMessages(ThreadType2.Group, cursor || null);
1979
2173
  return true;
1980
2174
  };
1981
2175
  const cleanup = () => {
@@ -2007,7 +2201,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
2007
2201
  }
2008
2202
  };
2009
2203
  const onOldMessages = (messages, type) => {
2010
- if (type !== ThreadType.Group) return;
2204
+ if (type !== ThreadType2.Group) return;
2011
2205
  const typedMessages = messages;
2012
2206
  for (const message of typedMessages) {
2013
2207
  if (message.threadId === threadId) {
@@ -2083,7 +2277,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
2083
2277
  requestedCursors.add(cursor);
2084
2278
  }
2085
2279
  pagesRequested += 1;
2086
- api.listener.requestOldMessages(ThreadType.User, cursor || null);
2280
+ api.listener.requestOldMessages(ThreadType2.User, cursor || null);
2087
2281
  return true;
2088
2282
  };
2089
2283
  const cleanup = () => {
@@ -2115,7 +2309,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
2115
2309
  }
2116
2310
  };
2117
2311
  const onOldMessages = (messages, type) => {
2118
- if (type !== ThreadType.User) return;
2312
+ if (type !== ThreadType2.User) return;
2119
2313
  const typedMessages = messages;
2120
2314
  for (const message of typedMessages) {
2121
2315
  if (message.threadId === threadId) {
@@ -3135,21 +3329,19 @@ auth.command("cache-clear").description("Clear local cache").action(
3135
3329
  })
3136
3330
  );
3137
3331
  var msg = program.command("msg").description("Messaging commands");
3138
- msg.command("send <threadId> <message>").option("-g, --group", "Send to group").option("--raw", "Send raw text without parsing formatting markers").description("Send text message with formatting (**bold** *italic* __bold__ ~~strike~~ {underline}text{/underline} {red}color{/red} {big}size{/big} lists indents)").action(
3332
+ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").option("--raw", "Send raw text without parsing formatting markers").description("Send text message with formatting (**bold** *italic* __bold__ ~~strike~~ {underline}text{/underline} {red}color{/red} {big}size{/big} lists indents). Group sends also resolve unique @Name mentions.").action(
3139
3333
  wrapAction(async (threadId, message, opts, command) => {
3140
3334
  const { api } = await requireApi(command);
3141
- if (opts.raw) {
3142
- const response = await api.sendMessage(message, threadId, asThreadType(opts.group));
3143
- output(response, false);
3144
- } else {
3145
- const { text, styles } = parseTextStyles(message);
3146
- const response = await api.sendMessage(
3147
- { msg: text, styles: styles.length > 0 ? styles : void 0 },
3148
- threadId,
3149
- asThreadType(opts.group)
3150
- );
3151
- output(response, false);
3152
- }
3335
+ const threadType = asThreadType(opts.group);
3336
+ const payload = await buildTextSendPayload({
3337
+ message,
3338
+ raw: opts.raw,
3339
+ threadType,
3340
+ threadId,
3341
+ listGroupMembers: threadType === ThreadType2.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
3342
+ });
3343
+ const response = await api.sendMessage(payload, threadId, threadType);
3344
+ output(response, false);
3153
3345
  })
3154
3346
  );
3155
3347
  msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("-g, --group", "Send to group").description("Send image(s) from file or URL").action(
@@ -3443,8 +3635,8 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
3443
3635
  {
3444
3636
  threadId,
3445
3637
  explicitGroupFlag: Boolean(opts.group),
3446
- isGroup: threadResolution.type === ThreadType.Group,
3447
- threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
3638
+ isGroup: threadResolution.type === ThreadType2.Group,
3639
+ threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
3448
3640
  threadTypeReason: threadResolution.reason,
3449
3641
  localFiles,
3450
3642
  urlInputs
@@ -3472,7 +3664,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
3472
3664
  "msg.upload.ipc.done",
3473
3665
  {
3474
3666
  threadId,
3475
- threadType: threadResolution.type === ThreadType.Group ? "group" : "user"
3667
+ threadType: threadResolution.type === ThreadType2.Group ? "group" : "user"
3476
3668
  },
3477
3669
  command
3478
3670
  );
@@ -3483,7 +3675,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
3483
3675
  "msg.upload.ipc.fallback",
3484
3676
  {
3485
3677
  threadId,
3486
- threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
3678
+ threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
3487
3679
  reason: ipcResult.reason
3488
3680
  },
3489
3681
  command
@@ -3522,7 +3714,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
3522
3714
  const { api } = await requireApi(command);
3523
3715
  const parsedCount = Number(opts.count);
3524
3716
  const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
3525
- const threadType = opts.group ? ThreadType.Group : ThreadType.User;
3717
+ const threadType = opts.group ? ThreadType2.Group : ThreadType2.User;
3526
3718
  const messages = opts.group ? await fetchRecentGroupMessagesViaApi(api, threadId, count) : await fetchRecentUserMessagesViaListener(
3527
3719
  api,
3528
3720
  threadId,
@@ -3532,7 +3724,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
3532
3724
  msgId: message.data.msgId,
3533
3725
  cliMsgId: message.data.cliMsgId,
3534
3726
  threadId: message.threadId || threadId,
3535
- threadType: message.type === ThreadType.Group ? "group" : "user",
3727
+ threadType: message.type === ThreadType2.Group ? "group" : "user",
3536
3728
  senderId: message.data.uidFrom,
3537
3729
  senderName: message.data.dName,
3538
3730
  ts: message.data.ts,
@@ -3541,7 +3733,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
3541
3733
  msgId: message.data.msgId,
3542
3734
  cliMsgId: message.data.cliMsgId,
3543
3735
  threadId: message.threadId || threadId,
3544
- group: message.type === ThreadType.Group
3736
+ group: message.type === ThreadType2.Group
3545
3737
  },
3546
3738
  content: typeof message.data.content === "string" ? message.data.content : JSON.stringify(message.data.content)
3547
3739
  }));
@@ -3549,7 +3741,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
3549
3741
  output(
3550
3742
  {
3551
3743
  threadId,
3552
- threadType: threadType === ThreadType.Group ? "group" : "user",
3744
+ threadType: threadType === ThreadType2.Group ? "group" : "user",
3553
3745
  count: rows.length,
3554
3746
  messages: rows
3555
3747
  },
@@ -3569,7 +3761,7 @@ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").de
3569
3761
  output(
3570
3762
  {
3571
3763
  threadId,
3572
- threadType: type === ThreadType.Group ? "group" : "user",
3764
+ threadType: type === ThreadType2.Group ? "group" : "user",
3573
3765
  pinned: true,
3574
3766
  response
3575
3767
  },
@@ -3585,7 +3777,7 @@ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation"
3585
3777
  output(
3586
3778
  {
3587
3779
  threadId,
3588
- threadType: type === ThreadType.Group ? "group" : "user",
3780
+ threadType: type === ThreadType2.Group ? "group" : "user",
3589
3781
  pinned: false,
3590
3782
  response
3591
3783
  },
@@ -3659,59 +3851,7 @@ group.command("info <groupId>").description("Get group info").action(
3659
3851
  group.command("members <groupId>").option("-j, --json", "JSON output").description("List group members").action(
3660
3852
  wrapAction(async (groupId, opts, command) => {
3661
3853
  const { api } = await requireApi(command);
3662
- const info = await api.getGroupInfo(groupId);
3663
- const groupInfo = info.gridInfoMap[groupId];
3664
- if (!groupInfo) {
3665
- throw new Error(`Group not found: ${groupId}`);
3666
- }
3667
- const normalizeMemberId = (value) => {
3668
- if (typeof value === "number" && Number.isFinite(value)) {
3669
- return String(Math.trunc(value));
3670
- }
3671
- if (typeof value !== "string") return "";
3672
- const trimmed = value.trim();
3673
- if (!trimmed) return "";
3674
- return trimmed.replace(/_\d+$/, "");
3675
- };
3676
- const idsFromMemberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
3677
- const memVerList = groupInfo.memVerList;
3678
- const idsFromMemVerList = Array.isArray(memVerList) ? memVerList.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
3679
- const currentMems = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
3680
- const currentMemberMap = /* @__PURE__ */ new Map();
3681
- for (const member of currentMems) {
3682
- const userId = normalizeMemberId(member.id);
3683
- if (!userId) continue;
3684
- currentMemberMap.set(userId, {
3685
- displayName: member.dName?.trim() || member.zaloName?.trim() || "",
3686
- zaloName: member.zaloName?.trim() || ""
3687
- });
3688
- }
3689
- const ids = Array.from(
3690
- /* @__PURE__ */ new Set([
3691
- ...idsFromMemberIds,
3692
- ...idsFromMemVerList,
3693
- ...Array.from(currentMemberMap.keys())
3694
- ])
3695
- );
3696
- const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
3697
- const rawProfileMap = profiles.profiles;
3698
- const profileMap = /* @__PURE__ */ new Map();
3699
- for (const [key, profile] of Object.entries(rawProfileMap)) {
3700
- if (!profile) continue;
3701
- const normalizedKey = normalizeMemberId(key);
3702
- if (normalizedKey && !profileMap.has(normalizedKey)) {
3703
- profileMap.set(normalizedKey, profile);
3704
- }
3705
- const profileId = normalizeMemberId(profile.id);
3706
- if (profileId && !profileMap.has(profileId)) {
3707
- profileMap.set(profileId, profile);
3708
- }
3709
- }
3710
- const rows = ids.map((id) => ({
3711
- userId: id,
3712
- displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
3713
- zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
3714
- }));
3854
+ const rows = await listGroupMemberRows(api, groupId);
3715
3855
  if (opts.json) {
3716
3856
  output(rows, true);
3717
3857
  return;
@@ -4430,7 +4570,7 @@ ${replyMediaText}` : replyMediaText;
4430
4570
  processedText = processedText.trim() ? `${processedText}
4431
4571
  ${replyContextText}` : replyContextText;
4432
4572
  }
4433
- const chatType = message.type === ThreadType.Group ? "group" : "user";
4573
+ const chatType = message.type === ThreadType2.Group ? "group" : "user";
4434
4574
  const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
4435
4575
  const senderDisplayNameRaw = getStringCandidate(messageData, [
4436
4576
  "dName",
@@ -4439,7 +4579,7 @@ ${replyContextText}` : replyContextText;
4439
4579
  "displayName"
4440
4580
  ]);
4441
4581
  const senderDisplayName = senderDisplayNameRaw || void 0;
4442
- const senderNameForMetadata = message.type === ThreadType.Group ? senderDisplayName : void 0;
4582
+ const senderNameForMetadata = message.type === ThreadType2.Group ? senderDisplayName : void 0;
4443
4583
  const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
4444
4584
  const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
4445
4585
  const mentions = extractInboundMentions({
@@ -4476,7 +4616,7 @@ ${replyContextText}` : replyContextText;
4476
4616
  mentions: mentions.length > 0 ? mentions : void 0,
4477
4617
  mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
4478
4618
  metadata: {
4479
- isGroup: message.type === ThreadType.Group,
4619
+ isGroup: message.type === ThreadType2.Group,
4480
4620
  chatType,
4481
4621
  threadId: message.threadId,
4482
4622
  targetId: message.threadId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {