openzca 0.1.27 → 0.1.29
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 +4 -1
- package/dist/cli.js +227 -25
- 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
|
|
79
|
+
| `openzca msg recent <threadId>` | List recent messages (`-n`, `--json`, newest-first); group mode prefers direct group-history endpoint (websocket fallback) |
|
|
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`).
|
|
@@ -275,6 +275,9 @@ Listener resilience override:
|
|
|
275
275
|
- `OPENZCA_RECENT_USER_MAX_PAGES`: max websocket history pages to scan for `msg recent` in user/DM mode.
|
|
276
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 path fails.
|
|
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
|
@@ -1584,28 +1584,132 @@ function sortRecentMessagesNewestFirst(messages) {
|
|
|
1584
1584
|
}
|
|
1585
1585
|
function getRecentMessageCursor(message) {
|
|
1586
1586
|
if (!message) return "";
|
|
1587
|
+
const msgId = String(message.data?.msgId ?? "").trim();
|
|
1588
|
+
if (msgId) return msgId;
|
|
1587
1589
|
const actionId = String(message.data?.actionId ?? "").trim();
|
|
1588
1590
|
if (actionId) return actionId;
|
|
1589
|
-
return String(message.data?.
|
|
1591
|
+
return String(message.data?.cliMsgId ?? "").trim();
|
|
1592
|
+
}
|
|
1593
|
+
function getOldestRecentMessage(messages) {
|
|
1594
|
+
let oldest = null;
|
|
1595
|
+
for (const message of messages) {
|
|
1596
|
+
if (!oldest) {
|
|
1597
|
+
oldest = message;
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (parseRecentMessageTs(message.data?.ts) < parseRecentMessageTs(oldest.data?.ts)) {
|
|
1601
|
+
oldest = message;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return oldest;
|
|
1605
|
+
}
|
|
1606
|
+
function getRecentPageCursors(messages) {
|
|
1607
|
+
const cursors = [];
|
|
1608
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1609
|
+
const addCursor = (value) => {
|
|
1610
|
+
const cursor = value.trim();
|
|
1611
|
+
if (!cursor || seen.has(cursor)) return;
|
|
1612
|
+
seen.add(cursor);
|
|
1613
|
+
cursors.push(cursor);
|
|
1614
|
+
};
|
|
1615
|
+
addCursor(getRecentMessageCursor(getOldestRecentMessage(messages)));
|
|
1616
|
+
addCursor(getRecentMessageCursor(messages[messages.length - 1] ?? null));
|
|
1617
|
+
addCursor(getRecentMessageCursor(messages[0] ?? null));
|
|
1618
|
+
return cursors;
|
|
1619
|
+
}
|
|
1620
|
+
function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
|
|
1621
|
+
const normalized = [];
|
|
1622
|
+
for (const message of messages) {
|
|
1623
|
+
if (!message || typeof message !== "object") continue;
|
|
1624
|
+
const raw = message;
|
|
1625
|
+
if (raw.data && raw.threadId) {
|
|
1626
|
+
const wrapped = raw;
|
|
1627
|
+
normalized.push(wrapped);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const threadIdRaw = String(raw.idTo ?? "").trim();
|
|
1631
|
+
normalized.push({
|
|
1632
|
+
threadId: threadIdRaw || fallbackThreadId,
|
|
1633
|
+
type: ThreadType.Group,
|
|
1634
|
+
data: {
|
|
1635
|
+
actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
|
|
1636
|
+
msgId: String(raw.msgId ?? ""),
|
|
1637
|
+
cliMsgId: String(raw.cliMsgId ?? ""),
|
|
1638
|
+
uidFrom: String(raw.uidFrom ?? ""),
|
|
1639
|
+
dName: typeof raw.dName === "string" ? raw.dName : void 0,
|
|
1640
|
+
ts: String(raw.ts ?? ""),
|
|
1641
|
+
msgType: String(raw.msgType ?? ""),
|
|
1642
|
+
content: raw.content ?? ""
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
return normalized;
|
|
1647
|
+
}
|
|
1648
|
+
async function fetchRecentGroupMessagesViaCustomApi(api, threadId, count) {
|
|
1649
|
+
const customApi = api;
|
|
1650
|
+
if (typeof customApi.__openzcaGroupHistory !== "function") {
|
|
1651
|
+
if (typeof customApi.custom !== "function") {
|
|
1652
|
+
throw new Error("Current zca-js build does not expose API custom hooks.");
|
|
1653
|
+
}
|
|
1654
|
+
customApi.custom("__openzcaGroupHistory", async ({ utils, props }) => {
|
|
1655
|
+
const serviceURL = utils.makeURL(`${api.zpwServiceMap.group[0]}/api/group/history`);
|
|
1656
|
+
const encryptedParams = utils.encodeAES(
|
|
1657
|
+
JSON.stringify({
|
|
1658
|
+
grid: props.groupId,
|
|
1659
|
+
count: props.count
|
|
1660
|
+
})
|
|
1661
|
+
);
|
|
1662
|
+
if (!encryptedParams) throw new Error("Failed to encrypt group history params.");
|
|
1663
|
+
const response2 = await utils.request(
|
|
1664
|
+
utils.makeURL(serviceURL, { params: encryptedParams }),
|
|
1665
|
+
{ method: "GET" }
|
|
1666
|
+
);
|
|
1667
|
+
return await utils.resolve(response2, (result) => {
|
|
1668
|
+
if (typeof result.data === "string") {
|
|
1669
|
+
try {
|
|
1670
|
+
return JSON.parse(result.data);
|
|
1671
|
+
} catch {
|
|
1672
|
+
return { groupMsgs: [] };
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
return result.data ?? { groupMsgs: [] };
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
const response = await customApi.__openzcaGroupHistory?.({
|
|
1680
|
+
groupId: threadId,
|
|
1681
|
+
count
|
|
1682
|
+
});
|
|
1683
|
+
const messagesRaw = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
|
|
1684
|
+
return sortRecentMessagesNewestFirst(
|
|
1685
|
+
normalizeGroupHistoryMessages(messagesRaw, threadId)
|
|
1686
|
+
).slice(0, count);
|
|
1590
1687
|
}
|
|
1591
1688
|
async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
|
|
1592
1689
|
const historyApi = api.getGroupChatHistory;
|
|
1593
|
-
if (typeof historyApi
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1690
|
+
if (typeof historyApi === "function") {
|
|
1691
|
+
try {
|
|
1692
|
+
const response = await historyApi(threadId, count);
|
|
1693
|
+
const messagesRaw = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
|
|
1694
|
+
return sortRecentMessagesNewestFirst(
|
|
1695
|
+
normalizeGroupHistoryMessages(messagesRaw, threadId)
|
|
1696
|
+
).slice(0, count);
|
|
1697
|
+
} catch {
|
|
1698
|
+
}
|
|
1597
1699
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1700
|
+
try {
|
|
1701
|
+
return await fetchRecentGroupMessagesViaCustomApi(api, threadId, count);
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
return fetchRecentGroupMessagesViaListener(api, threadId, count);
|
|
1601
1705
|
}
|
|
1602
|
-
async function
|
|
1706
|
+
async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
1603
1707
|
return new Promise((resolve, reject) => {
|
|
1604
1708
|
let settled = false;
|
|
1605
1709
|
const collected = [];
|
|
1606
1710
|
const seenMessageKeys = /* @__PURE__ */ new Set();
|
|
1607
1711
|
const requestedCursors = /* @__PURE__ */ new Set();
|
|
1608
|
-
const maxPages = parsePositiveIntFromEnv("
|
|
1712
|
+
const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_GROUP_MAX_PAGES", 20);
|
|
1609
1713
|
let pagesRequested = 0;
|
|
1610
1714
|
const toKey = (message) => {
|
|
1611
1715
|
const msgId = String(message.data?.msgId ?? "");
|
|
@@ -1619,7 +1723,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
1619
1723
|
requestedCursors.add(cursor);
|
|
1620
1724
|
}
|
|
1621
1725
|
pagesRequested += 1;
|
|
1622
|
-
api.listener.requestOldMessages(ThreadType.
|
|
1726
|
+
api.listener.requestOldMessages(ThreadType.Group, cursor || null);
|
|
1623
1727
|
return true;
|
|
1624
1728
|
};
|
|
1625
1729
|
const cleanup = () => {
|
|
@@ -1651,7 +1755,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
1651
1755
|
}
|
|
1652
1756
|
};
|
|
1653
1757
|
const onOldMessages = (messages, type) => {
|
|
1654
|
-
if (type !== ThreadType.
|
|
1758
|
+
if (type !== ThreadType.Group) return;
|
|
1655
1759
|
const typedMessages = messages;
|
|
1656
1760
|
for (const message of typedMessages) {
|
|
1657
1761
|
if (message.threadId === threadId) {
|
|
@@ -1673,26 +1777,124 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
1673
1777
|
finish();
|
|
1674
1778
|
return;
|
|
1675
1779
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1780
|
+
try {
|
|
1781
|
+
const cursorCandidates = getRecentPageCursors(typedMessages);
|
|
1782
|
+
let requested = false;
|
|
1783
|
+
for (const cursor of cursorCandidates) {
|
|
1784
|
+
if (requestPage(cursor)) {
|
|
1785
|
+
requested = true;
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1681
1788
|
}
|
|
1682
|
-
if (
|
|
1683
|
-
|
|
1789
|
+
if (!requested) finish();
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
finish(error);
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
const onError = (error) => {
|
|
1795
|
+
finish(error);
|
|
1796
|
+
};
|
|
1797
|
+
const onClosed = () => {
|
|
1798
|
+
finish();
|
|
1799
|
+
};
|
|
1800
|
+
const timeoutId = setTimeout(() => {
|
|
1801
|
+
finish();
|
|
1802
|
+
}, 12e3);
|
|
1803
|
+
api.listener.on("connected", onConnected);
|
|
1804
|
+
api.listener.on("old_messages", onOldMessages);
|
|
1805
|
+
api.listener.on("error", onError);
|
|
1806
|
+
api.listener.on("closed", onClosed);
|
|
1807
|
+
try {
|
|
1808
|
+
api.listener.start();
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
finish(error);
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
1815
|
+
return new Promise((resolve, reject) => {
|
|
1816
|
+
let settled = false;
|
|
1817
|
+
const collected = [];
|
|
1818
|
+
const seenMessageKeys = /* @__PURE__ */ new Set();
|
|
1819
|
+
const requestedCursors = /* @__PURE__ */ new Set();
|
|
1820
|
+
const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_USER_MAX_PAGES", 20);
|
|
1821
|
+
let pagesRequested = 0;
|
|
1822
|
+
const toKey = (message) => {
|
|
1823
|
+
const msgId = String(message.data?.msgId ?? "");
|
|
1824
|
+
const cliMsgId = String(message.data?.cliMsgId ?? "");
|
|
1825
|
+
return `${msgId}:${cliMsgId}`;
|
|
1826
|
+
};
|
|
1827
|
+
const requestPage = (lastId) => {
|
|
1828
|
+
const cursor = String(lastId ?? "").trim();
|
|
1829
|
+
if (cursor) {
|
|
1830
|
+
if (requestedCursors.has(cursor)) return false;
|
|
1831
|
+
requestedCursors.add(cursor);
|
|
1832
|
+
}
|
|
1833
|
+
pagesRequested += 1;
|
|
1834
|
+
api.listener.requestOldMessages(ThreadType.User, cursor || null);
|
|
1835
|
+
return true;
|
|
1836
|
+
};
|
|
1837
|
+
const cleanup = () => {
|
|
1838
|
+
clearTimeout(timeoutId);
|
|
1839
|
+
api.listener.off("connected", onConnected);
|
|
1840
|
+
api.listener.off("old_messages", onOldMessages);
|
|
1841
|
+
api.listener.off("error", onError);
|
|
1842
|
+
api.listener.off("closed", onClosed);
|
|
1843
|
+
try {
|
|
1844
|
+
api.listener.stop();
|
|
1845
|
+
} catch {
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
const finish = (error) => {
|
|
1849
|
+
if (settled) return;
|
|
1850
|
+
settled = true;
|
|
1851
|
+
cleanup();
|
|
1852
|
+
if (error) {
|
|
1853
|
+
reject(error);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
resolve(sortRecentMessagesNewestFirst(collected).slice(0, count));
|
|
1857
|
+
};
|
|
1858
|
+
const onConnected = () => {
|
|
1859
|
+
try {
|
|
1860
|
+
requestPage(null);
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
finish(error);
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
const onOldMessages = (messages, type) => {
|
|
1866
|
+
if (type !== ThreadType.User) return;
|
|
1867
|
+
const typedMessages = messages;
|
|
1868
|
+
for (const message of typedMessages) {
|
|
1869
|
+
if (message.threadId === threadId) {
|
|
1870
|
+
const key = toKey(message);
|
|
1871
|
+
if (seenMessageKeys.has(key)) continue;
|
|
1872
|
+
seenMessageKeys.add(key);
|
|
1873
|
+
collected.push(message);
|
|
1684
1874
|
}
|
|
1685
1875
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1876
|
+
if (collected.length >= count) {
|
|
1877
|
+
finish();
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (typedMessages.length === 0) {
|
|
1881
|
+
finish();
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (pagesRequested >= maxPages) {
|
|
1688
1885
|
finish();
|
|
1689
1886
|
return;
|
|
1690
1887
|
}
|
|
1691
1888
|
try {
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1889
|
+
const cursorCandidates = getRecentPageCursors(typedMessages);
|
|
1890
|
+
let requested = false;
|
|
1891
|
+
for (const cursor of cursorCandidates) {
|
|
1892
|
+
if (requestPage(cursor)) {
|
|
1893
|
+
requested = true;
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1695
1896
|
}
|
|
1897
|
+
if (!requested) finish();
|
|
1696
1898
|
} catch (error) {
|
|
1697
1899
|
finish(error);
|
|
1698
1900
|
}
|
|
@@ -3052,7 +3254,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3052
3254
|
}
|
|
3053
3255
|
)
|
|
3054
3256
|
);
|
|
3055
|
-
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(
|
|
3257
|
+
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(
|
|
3056
3258
|
wrapAction(
|
|
3057
3259
|
async (threadId, opts, command) => {
|
|
3058
3260
|
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.29",
|
|
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
|
}
|