openzca 0.1.25 → 0.1.26

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 -1
  2. package/dist/cli.js +86 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -76,7 +76,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
76
76
  | `openzca msg delete <msgId> <cliMsgId> <uidFrom> <threadId>` | Delete a message |
77
77
  | `openzca msg undo <msgId> <cliMsgId> <threadId>` | Recall a sent message |
78
78
  | `openzca msg upload <arg1> [arg2]` | Upload and send file(s) |
79
- | `openzca msg recent <threadId>` | List recent messages (`-n`, `--json`); group mode uses direct group-history API |
79
+ | `openzca msg recent <threadId>` | List recent messages (`-n`, `--json`, newest-first); group mode uses direct group-history API |
80
80
 
81
81
  Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
82
82
  Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
@@ -272,6 +272,9 @@ Listener resilience override:
272
272
  - `OPENZCA_LISTEN_DOWNLOAD_QUOTE_MEDIA`: download quoted attachment URLs (if present) into inbound media cache.
273
273
  - Default: enabled.
274
274
  - Set to `0` to keep only quote metadata/URLs without downloading.
275
+ - `OPENZCA_RECENT_USER_MAX_PAGES`: max websocket history pages to scan for `msg recent` in user/DM mode.
276
+ - Default: `6`.
277
+ - Increase if a DM thread is old and not found in the first page.
275
278
  - `OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER`: enforce one `listen` owner process per profile.
276
279
  - Default: enabled.
277
280
  - Set to `0` to allow multiple listeners on the same profile (not recommended).
package/dist/cli.js CHANGED
@@ -1555,6 +1555,33 @@ async function withUploadListener(api, command, task) {
1555
1555
  api.listener.off("closed", sinkClosed);
1556
1556
  }
1557
1557
  }
1558
+ function parseRecentMessageTs(value) {
1559
+ if (typeof value === "number" && Number.isFinite(value)) {
1560
+ return Math.trunc(value);
1561
+ }
1562
+ if (typeof value === "string") {
1563
+ const trimmed = value.trim();
1564
+ if (!trimmed) return 0;
1565
+ const parsed = Number(trimmed);
1566
+ if (Number.isFinite(parsed)) {
1567
+ return Math.trunc(parsed);
1568
+ }
1569
+ }
1570
+ return 0;
1571
+ }
1572
+ function sortRecentMessagesNewestFirst(messages) {
1573
+ return [...messages].sort((left, right) => {
1574
+ const rightTs = parseRecentMessageTs(right.data?.ts);
1575
+ const leftTs = parseRecentMessageTs(left.data?.ts);
1576
+ if (rightTs !== leftTs) return rightTs - leftTs;
1577
+ const rightMsgId = String(right.data?.msgId ?? "");
1578
+ const leftMsgId = String(left.data?.msgId ?? "");
1579
+ if (rightMsgId !== leftMsgId) return rightMsgId.localeCompare(leftMsgId);
1580
+ const rightCliMsgId = String(right.data?.cliMsgId ?? "");
1581
+ const leftCliMsgId = String(left.data?.cliMsgId ?? "");
1582
+ return rightCliMsgId.localeCompare(leftCliMsgId);
1583
+ });
1584
+ }
1558
1585
  async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
1559
1586
  const historyApi = api.getGroupChatHistory;
1560
1587
  if (typeof historyApi !== "function") {
@@ -1564,12 +1591,30 @@ async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
1564
1591
  }
1565
1592
  const response = await historyApi(threadId, count);
1566
1593
  const messages = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
1567
- return messages.slice(0, count);
1594
+ return sortRecentMessagesNewestFirst(messages).slice(0, count);
1568
1595
  }
1569
1596
  async function fetchRecentUserMessagesViaListener(api, threadId, count) {
1570
1597
  return new Promise((resolve, reject) => {
1571
1598
  let settled = false;
1572
1599
  const collected = [];
1600
+ const seenMessageKeys = /* @__PURE__ */ new Set();
1601
+ const requestedLastIds = /* @__PURE__ */ new Set();
1602
+ const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_USER_MAX_PAGES", 6);
1603
+ let pagesRequested = 0;
1604
+ const toKey = (message) => {
1605
+ const msgId = String(message.data?.msgId ?? "");
1606
+ const cliMsgId = String(message.data?.cliMsgId ?? "");
1607
+ return `${msgId}:${cliMsgId}`;
1608
+ };
1609
+ const requestPage = (lastMsgId) => {
1610
+ if (lastMsgId) {
1611
+ if (requestedLastIds.has(lastMsgId)) return false;
1612
+ requestedLastIds.add(lastMsgId);
1613
+ }
1614
+ pagesRequested += 1;
1615
+ api.listener.requestOldMessages(ThreadType.User, lastMsgId);
1616
+ return true;
1617
+ };
1573
1618
  const cleanup = () => {
1574
1619
  clearTimeout(timeoutId);
1575
1620
  api.listener.off("connected", onConnected);
@@ -1589,11 +1634,11 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
1589
1634
  reject(error);
1590
1635
  return;
1591
1636
  }
1592
- resolve(collected.slice(0, count));
1637
+ resolve(sortRecentMessagesNewestFirst(collected).slice(0, count));
1593
1638
  };
1594
1639
  const onConnected = () => {
1595
1640
  try {
1596
- api.listener.requestOldMessages(ThreadType.User, null);
1641
+ requestPage(null);
1597
1642
  } catch (error) {
1598
1643
  finish(error);
1599
1644
  }
@@ -1603,10 +1648,47 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
1603
1648
  const typedMessages = messages;
1604
1649
  for (const message of typedMessages) {
1605
1650
  if (message.threadId === threadId) {
1651
+ const key = toKey(message);
1652
+ if (seenMessageKeys.has(key)) continue;
1653
+ seenMessageKeys.add(key);
1606
1654
  collected.push(message);
1607
1655
  }
1608
1656
  }
1609
- finish();
1657
+ if (collected.length >= count) {
1658
+ finish();
1659
+ return;
1660
+ }
1661
+ if (typedMessages.length === 0) {
1662
+ finish();
1663
+ return;
1664
+ }
1665
+ if (pagesRequested >= maxPages) {
1666
+ finish();
1667
+ return;
1668
+ }
1669
+ let oldest = null;
1670
+ for (const message of typedMessages) {
1671
+ if (!oldest) {
1672
+ oldest = message;
1673
+ continue;
1674
+ }
1675
+ if (parseRecentMessageTs(message.data?.ts) < parseRecentMessageTs(oldest.data?.ts)) {
1676
+ oldest = message;
1677
+ }
1678
+ }
1679
+ const nextLastId = String(oldest?.data?.msgId ?? "").trim();
1680
+ if (!nextLastId) {
1681
+ finish();
1682
+ return;
1683
+ }
1684
+ try {
1685
+ const requested = requestPage(nextLastId);
1686
+ if (!requested) {
1687
+ finish();
1688
+ }
1689
+ } catch (error) {
1690
+ finish(error);
1691
+ }
1610
1692
  };
1611
1693
  const onError = (error) => {
1612
1694
  finish(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {