openzca 0.1.26 → 0.1.28
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 +5 -2
- package/dist/cli.js +142 -27
- package/package.json +5 -5
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`, newest-first); 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 when available (fallback: websocket history) |
|
|
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`).
|
|
@@ -273,8 +273,11 @@ Listener resilience override:
|
|
|
273
273
|
- Default: enabled.
|
|
274
274
|
- Set to `0` to keep only quote metadata/URLs without downloading.
|
|
275
275
|
- `OPENZCA_RECENT_USER_MAX_PAGES`: max websocket history pages to scan for `msg recent` in user/DM mode.
|
|
276
|
-
- Default: `
|
|
276
|
+
- Default: `20`.
|
|
277
277
|
- Increase if a DM thread is old and not found in the first page.
|
|
278
|
+
- `OPENZCA_RECENT_GROUP_MAX_PAGES`: max websocket history pages to scan for `msg recent -g` when direct group-history API is unavailable.
|
|
279
|
+
- Default: `20`.
|
|
280
|
+
- Increase if a group thread is old and not found quickly.
|
|
278
281
|
- `OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER`: enforce one `listen` owner process per profile.
|
|
279
282
|
- Default: enabled.
|
|
280
283
|
- Set to `0` to allow multiple listeners on the same profile (not recommended).
|
package/dist/cli.js
CHANGED
|
@@ -1582,37 +1582,54 @@ function sortRecentMessagesNewestFirst(messages) {
|
|
|
1582
1582
|
return rightCliMsgId.localeCompare(leftCliMsgId);
|
|
1583
1583
|
});
|
|
1584
1584
|
}
|
|
1585
|
+
function getRecentMessageCursor(message) {
|
|
1586
|
+
if (!message) return "";
|
|
1587
|
+
const msgId = String(message.data?.msgId ?? "").trim();
|
|
1588
|
+
if (msgId) return msgId;
|
|
1589
|
+
const actionId = String(message.data?.actionId ?? "").trim();
|
|
1590
|
+
if (actionId) return actionId;
|
|
1591
|
+
return String(message.data?.cliMsgId ?? "").trim();
|
|
1592
|
+
}
|
|
1593
|
+
function getRecentPageCursor(messages) {
|
|
1594
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1595
|
+
const cursor = getRecentMessageCursor(messages[index] ?? null);
|
|
1596
|
+
if (cursor) return cursor;
|
|
1597
|
+
}
|
|
1598
|
+
return "";
|
|
1599
|
+
}
|
|
1585
1600
|
async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
|
|
1586
1601
|
const historyApi = api.getGroupChatHistory;
|
|
1587
|
-
if (typeof historyApi
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1602
|
+
if (typeof historyApi === "function") {
|
|
1603
|
+
try {
|
|
1604
|
+
const response = await historyApi(threadId, count);
|
|
1605
|
+
const messages = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
|
|
1606
|
+
return sortRecentMessagesNewestFirst(messages).slice(0, count);
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1591
1609
|
}
|
|
1592
|
-
|
|
1593
|
-
const messages = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
|
|
1594
|
-
return sortRecentMessagesNewestFirst(messages).slice(0, count);
|
|
1610
|
+
return fetchRecentGroupMessagesViaListener(api, threadId, count);
|
|
1595
1611
|
}
|
|
1596
|
-
async function
|
|
1612
|
+
async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
1597
1613
|
return new Promise((resolve, reject) => {
|
|
1598
1614
|
let settled = false;
|
|
1599
1615
|
const collected = [];
|
|
1600
1616
|
const seenMessageKeys = /* @__PURE__ */ new Set();
|
|
1601
|
-
const
|
|
1602
|
-
const maxPages = parsePositiveIntFromEnv("
|
|
1617
|
+
const requestedCursors = /* @__PURE__ */ new Set();
|
|
1618
|
+
const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_GROUP_MAX_PAGES", 20);
|
|
1603
1619
|
let pagesRequested = 0;
|
|
1604
1620
|
const toKey = (message) => {
|
|
1605
1621
|
const msgId = String(message.data?.msgId ?? "");
|
|
1606
1622
|
const cliMsgId = String(message.data?.cliMsgId ?? "");
|
|
1607
1623
|
return `${msgId}:${cliMsgId}`;
|
|
1608
1624
|
};
|
|
1609
|
-
const requestPage = (
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1625
|
+
const requestPage = (lastId) => {
|
|
1626
|
+
const cursor = String(lastId ?? "").trim();
|
|
1627
|
+
if (cursor) {
|
|
1628
|
+
if (requestedCursors.has(cursor)) return false;
|
|
1629
|
+
requestedCursors.add(cursor);
|
|
1613
1630
|
}
|
|
1614
1631
|
pagesRequested += 1;
|
|
1615
|
-
api.listener.requestOldMessages(ThreadType.
|
|
1632
|
+
api.listener.requestOldMessages(ThreadType.Group, cursor || null);
|
|
1616
1633
|
return true;
|
|
1617
1634
|
};
|
|
1618
1635
|
const cleanup = () => {
|
|
@@ -1644,7 +1661,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
1644
1661
|
}
|
|
1645
1662
|
};
|
|
1646
1663
|
const onOldMessages = (messages, type) => {
|
|
1647
|
-
if (type !== ThreadType.
|
|
1664
|
+
if (type !== ThreadType.Group) return;
|
|
1648
1665
|
const typedMessages = messages;
|
|
1649
1666
|
for (const message of typedMessages) {
|
|
1650
1667
|
if (message.threadId === threadId) {
|
|
@@ -1666,23 +1683,121 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
1666
1683
|
finish();
|
|
1667
1684
|
return;
|
|
1668
1685
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1686
|
+
const nextCursor = getRecentPageCursor(typedMessages);
|
|
1687
|
+
if (!nextCursor) {
|
|
1688
|
+
finish();
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
try {
|
|
1692
|
+
const requested = requestPage(nextCursor);
|
|
1693
|
+
if (!requested) {
|
|
1694
|
+
finish();
|
|
1674
1695
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
finish(error);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
const onError = (error) => {
|
|
1701
|
+
finish(error);
|
|
1702
|
+
};
|
|
1703
|
+
const onClosed = () => {
|
|
1704
|
+
finish();
|
|
1705
|
+
};
|
|
1706
|
+
const timeoutId = setTimeout(() => {
|
|
1707
|
+
finish();
|
|
1708
|
+
}, 12e3);
|
|
1709
|
+
api.listener.on("connected", onConnected);
|
|
1710
|
+
api.listener.on("old_messages", onOldMessages);
|
|
1711
|
+
api.listener.on("error", onError);
|
|
1712
|
+
api.listener.on("closed", onClosed);
|
|
1713
|
+
try {
|
|
1714
|
+
api.listener.start();
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
finish(error);
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
1721
|
+
return new Promise((resolve, reject) => {
|
|
1722
|
+
let settled = false;
|
|
1723
|
+
const collected = [];
|
|
1724
|
+
const seenMessageKeys = /* @__PURE__ */ new Set();
|
|
1725
|
+
const requestedCursors = /* @__PURE__ */ new Set();
|
|
1726
|
+
const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_USER_MAX_PAGES", 20);
|
|
1727
|
+
let pagesRequested = 0;
|
|
1728
|
+
const toKey = (message) => {
|
|
1729
|
+
const msgId = String(message.data?.msgId ?? "");
|
|
1730
|
+
const cliMsgId = String(message.data?.cliMsgId ?? "");
|
|
1731
|
+
return `${msgId}:${cliMsgId}`;
|
|
1732
|
+
};
|
|
1733
|
+
const requestPage = (lastId) => {
|
|
1734
|
+
const cursor = String(lastId ?? "").trim();
|
|
1735
|
+
if (cursor) {
|
|
1736
|
+
if (requestedCursors.has(cursor)) return false;
|
|
1737
|
+
requestedCursors.add(cursor);
|
|
1738
|
+
}
|
|
1739
|
+
pagesRequested += 1;
|
|
1740
|
+
api.listener.requestOldMessages(ThreadType.User, cursor || null);
|
|
1741
|
+
return true;
|
|
1742
|
+
};
|
|
1743
|
+
const cleanup = () => {
|
|
1744
|
+
clearTimeout(timeoutId);
|
|
1745
|
+
api.listener.off("connected", onConnected);
|
|
1746
|
+
api.listener.off("old_messages", onOldMessages);
|
|
1747
|
+
api.listener.off("error", onError);
|
|
1748
|
+
api.listener.off("closed", onClosed);
|
|
1749
|
+
try {
|
|
1750
|
+
api.listener.stop();
|
|
1751
|
+
} catch {
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
const finish = (error) => {
|
|
1755
|
+
if (settled) return;
|
|
1756
|
+
settled = true;
|
|
1757
|
+
cleanup();
|
|
1758
|
+
if (error) {
|
|
1759
|
+
reject(error);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
resolve(sortRecentMessagesNewestFirst(collected).slice(0, count));
|
|
1763
|
+
};
|
|
1764
|
+
const onConnected = () => {
|
|
1765
|
+
try {
|
|
1766
|
+
requestPage(null);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
finish(error);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
const onOldMessages = (messages, type) => {
|
|
1772
|
+
if (type !== ThreadType.User) return;
|
|
1773
|
+
const typedMessages = messages;
|
|
1774
|
+
for (const message of typedMessages) {
|
|
1775
|
+
if (message.threadId === threadId) {
|
|
1776
|
+
const key = toKey(message);
|
|
1777
|
+
if (seenMessageKeys.has(key)) continue;
|
|
1778
|
+
seenMessageKeys.add(key);
|
|
1779
|
+
collected.push(message);
|
|
1677
1780
|
}
|
|
1678
1781
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1782
|
+
if (collected.length >= count) {
|
|
1783
|
+
finish();
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
if (typedMessages.length === 0) {
|
|
1787
|
+
finish();
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (pagesRequested >= maxPages) {
|
|
1791
|
+
finish();
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const nextCursor = getRecentPageCursor(typedMessages);
|
|
1795
|
+
if (!nextCursor) {
|
|
1681
1796
|
finish();
|
|
1682
1797
|
return;
|
|
1683
1798
|
}
|
|
1684
1799
|
try {
|
|
1685
|
-
const requested = requestPage(
|
|
1800
|
+
const requested = requestPage(nextCursor);
|
|
1686
1801
|
if (!requested) {
|
|
1687
1802
|
finish();
|
|
1688
1803
|
}
|
|
@@ -3045,7 +3160,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3045
3160
|
}
|
|
3046
3161
|
)
|
|
3047
3162
|
);
|
|
3048
|
-
msg.command("recent <threadId>").option("-g, --group", "List recent messages for group thread").option("-n, --count <count>", "Number of messages (default: 20)", "20").option("-j, --json", "JSON output").description("List recent messages (group uses direct history API)").action(
|
|
3163
|
+
msg.command("recent <threadId>").option("-g, --group", "List recent messages for group thread").option("-n, --count <count>", "Number of messages (default: 20)", "20").option("-j, --json", "JSON output").description("List recent messages (group uses direct history API when available)").action(
|
|
3049
3164
|
wrapAction(
|
|
3050
3165
|
async (threadId, opts, command) => {
|
|
3051
3166
|
const { api } = await requireApi(command);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzca",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@types/qrcode-terminal": "^0.12.2",
|
|
45
|
-
"commander": "^14.0.
|
|
45
|
+
"commander": "^14.0.3",
|
|
46
46
|
"image-size": "^2.0.2",
|
|
47
47
|
"qrcode-terminal": "^0.12.0",
|
|
48
48
|
"zca-js": "^2.0.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"tsup": "^8.5.
|
|
51
|
+
"@types/node": "^25.2.3",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
53
|
"tsx": "^4.21.0",
|
|
54
|
-
"typescript": "^5.9.
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
55
|
}
|
|
56
56
|
}
|