openzca 0.1.25 → 0.1.27

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 +93 -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: `20`.
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,39 @@ 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
+ }
1585
+ function getRecentMessageCursor(message) {
1586
+ if (!message) return "";
1587
+ const actionId = String(message.data?.actionId ?? "").trim();
1588
+ if (actionId) return actionId;
1589
+ return String(message.data?.msgId ?? "").trim();
1590
+ }
1558
1591
  async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
1559
1592
  const historyApi = api.getGroupChatHistory;
1560
1593
  if (typeof historyApi !== "function") {
@@ -1564,12 +1597,31 @@ async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
1564
1597
  }
1565
1598
  const response = await historyApi(threadId, count);
1566
1599
  const messages = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
1567
- return messages.slice(0, count);
1600
+ return sortRecentMessagesNewestFirst(messages).slice(0, count);
1568
1601
  }
1569
1602
  async function fetchRecentUserMessagesViaListener(api, threadId, count) {
1570
1603
  return new Promise((resolve, reject) => {
1571
1604
  let settled = false;
1572
1605
  const collected = [];
1606
+ const seenMessageKeys = /* @__PURE__ */ new Set();
1607
+ const requestedCursors = /* @__PURE__ */ new Set();
1608
+ const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_USER_MAX_PAGES", 20);
1609
+ let pagesRequested = 0;
1610
+ const toKey = (message) => {
1611
+ const msgId = String(message.data?.msgId ?? "");
1612
+ const cliMsgId = String(message.data?.cliMsgId ?? "");
1613
+ return `${msgId}:${cliMsgId}`;
1614
+ };
1615
+ const requestPage = (lastId) => {
1616
+ const cursor = String(lastId ?? "").trim();
1617
+ if (cursor) {
1618
+ if (requestedCursors.has(cursor)) return false;
1619
+ requestedCursors.add(cursor);
1620
+ }
1621
+ pagesRequested += 1;
1622
+ api.listener.requestOldMessages(ThreadType.User, cursor || null);
1623
+ return true;
1624
+ };
1573
1625
  const cleanup = () => {
1574
1626
  clearTimeout(timeoutId);
1575
1627
  api.listener.off("connected", onConnected);
@@ -1589,11 +1641,11 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
1589
1641
  reject(error);
1590
1642
  return;
1591
1643
  }
1592
- resolve(collected.slice(0, count));
1644
+ resolve(sortRecentMessagesNewestFirst(collected).slice(0, count));
1593
1645
  };
1594
1646
  const onConnected = () => {
1595
1647
  try {
1596
- api.listener.requestOldMessages(ThreadType.User, null);
1648
+ requestPage(null);
1597
1649
  } catch (error) {
1598
1650
  finish(error);
1599
1651
  }
@@ -1603,10 +1655,47 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
1603
1655
  const typedMessages = messages;
1604
1656
  for (const message of typedMessages) {
1605
1657
  if (message.threadId === threadId) {
1658
+ const key = toKey(message);
1659
+ if (seenMessageKeys.has(key)) continue;
1660
+ seenMessageKeys.add(key);
1606
1661
  collected.push(message);
1607
1662
  }
1608
1663
  }
1609
- finish();
1664
+ if (collected.length >= count) {
1665
+ finish();
1666
+ return;
1667
+ }
1668
+ if (typedMessages.length === 0) {
1669
+ finish();
1670
+ return;
1671
+ }
1672
+ if (pagesRequested >= maxPages) {
1673
+ finish();
1674
+ return;
1675
+ }
1676
+ let oldest = null;
1677
+ for (const message of typedMessages) {
1678
+ if (!oldest) {
1679
+ oldest = message;
1680
+ continue;
1681
+ }
1682
+ if (parseRecentMessageTs(message.data?.ts) < parseRecentMessageTs(oldest.data?.ts)) {
1683
+ oldest = message;
1684
+ }
1685
+ }
1686
+ const nextCursor = getRecentMessageCursor(oldest);
1687
+ if (!nextCursor) {
1688
+ finish();
1689
+ return;
1690
+ }
1691
+ try {
1692
+ const requested = requestPage(nextCursor);
1693
+ if (!requested) {
1694
+ finish();
1695
+ }
1696
+ } catch (error) {
1697
+ finish(error);
1698
+ }
1610
1699
  };
1611
1700
  const onError = (error) => {
1612
1701
  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.27",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {