openzca 0.1.28 → 0.1.30
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 +2 -2
- package/dist/cli.js +161 -27
- 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`, 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,7 +275,7 @@ 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
|
|
278
|
+
- `OPENZCA_RECENT_GROUP_MAX_PAGES`: max websocket history pages to scan for `msg recent -g` when direct group-history path fails.
|
|
279
279
|
- Default: `20`.
|
|
280
280
|
- Increase if a group thread is old and not found quickly.
|
|
281
281
|
- `OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER`: enforce one `listen` owner process per profile.
|
package/dist/cli.js
CHANGED
|
@@ -1590,23 +1590,117 @@ function getRecentMessageCursor(message) {
|
|
|
1590
1590
|
if (actionId) return actionId;
|
|
1591
1591
|
return String(message.data?.cliMsgId ?? "").trim();
|
|
1592
1592
|
}
|
|
1593
|
-
function
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
if (
|
|
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
|
+
}
|
|
1597
1603
|
}
|
|
1598
|
-
return
|
|
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);
|
|
1599
1687
|
}
|
|
1600
1688
|
async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
|
|
1601
1689
|
const historyApi = api.getGroupChatHistory;
|
|
1602
1690
|
if (typeof historyApi === "function") {
|
|
1603
1691
|
try {
|
|
1604
1692
|
const response = await historyApi(threadId, count);
|
|
1605
|
-
const
|
|
1606
|
-
return sortRecentMessagesNewestFirst(
|
|
1693
|
+
const messagesRaw = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
|
|
1694
|
+
return sortRecentMessagesNewestFirst(
|
|
1695
|
+
normalizeGroupHistoryMessages(messagesRaw, threadId)
|
|
1696
|
+
).slice(0, count);
|
|
1607
1697
|
} catch {
|
|
1608
1698
|
}
|
|
1609
1699
|
}
|
|
1700
|
+
try {
|
|
1701
|
+
return await fetchRecentGroupMessagesViaCustomApi(api, threadId, count);
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1610
1704
|
return fetchRecentGroupMessagesViaListener(api, threadId, count);
|
|
1611
1705
|
}
|
|
1612
1706
|
async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
@@ -1683,16 +1777,16 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
1683
1777
|
finish();
|
|
1684
1778
|
return;
|
|
1685
1779
|
}
|
|
1686
|
-
const nextCursor = getRecentPageCursor(typedMessages);
|
|
1687
|
-
if (!nextCursor) {
|
|
1688
|
-
finish();
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
1780
|
try {
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
+
}
|
|
1695
1788
|
}
|
|
1789
|
+
if (!requested) finish();
|
|
1696
1790
|
} catch (error) {
|
|
1697
1791
|
finish(error);
|
|
1698
1792
|
}
|
|
@@ -1791,16 +1885,16 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
1791
1885
|
finish();
|
|
1792
1886
|
return;
|
|
1793
1887
|
}
|
|
1794
|
-
const nextCursor = getRecentPageCursor(typedMessages);
|
|
1795
|
-
if (!nextCursor) {
|
|
1796
|
-
finish();
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
1888
|
try {
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
+
}
|
|
1803
1896
|
}
|
|
1897
|
+
if (!requested) finish();
|
|
1804
1898
|
} catch (error) {
|
|
1805
1899
|
finish(error);
|
|
1806
1900
|
}
|
|
@@ -3300,13 +3394,53 @@ group.command("members <groupId>").option("-j, --json", "JSON output").descripti
|
|
|
3300
3394
|
if (!groupInfo) {
|
|
3301
3395
|
throw new Error(`Group not found: ${groupId}`);
|
|
3302
3396
|
}
|
|
3303
|
-
const
|
|
3397
|
+
const normalizeMemberId = (value) => {
|
|
3398
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3399
|
+
return String(Math.trunc(value));
|
|
3400
|
+
}
|
|
3401
|
+
if (typeof value !== "string") return "";
|
|
3402
|
+
const trimmed = value.trim();
|
|
3403
|
+
if (!trimmed) return "";
|
|
3404
|
+
return trimmed.replace(/_\d+$/, "");
|
|
3405
|
+
};
|
|
3406
|
+
const idsFromMemberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
|
|
3407
|
+
const memVerList = groupInfo.memVerList;
|
|
3408
|
+
const idsFromMemVerList = Array.isArray(memVerList) ? memVerList.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
|
|
3409
|
+
const currentMems = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
|
|
3410
|
+
const currentMemberMap = /* @__PURE__ */ new Map();
|
|
3411
|
+
for (const member of currentMems) {
|
|
3412
|
+
const userId = normalizeMemberId(member.id);
|
|
3413
|
+
if (!userId) continue;
|
|
3414
|
+
currentMemberMap.set(userId, {
|
|
3415
|
+
displayName: member.dName?.trim() || member.zaloName?.trim() || "",
|
|
3416
|
+
zaloName: member.zaloName?.trim() || ""
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
const ids = Array.from(
|
|
3420
|
+
/* @__PURE__ */ new Set([
|
|
3421
|
+
...idsFromMemberIds,
|
|
3422
|
+
...idsFromMemVerList,
|
|
3423
|
+
...Array.from(currentMemberMap.keys())
|
|
3424
|
+
])
|
|
3425
|
+
);
|
|
3304
3426
|
const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
|
|
3305
|
-
const
|
|
3427
|
+
const rawProfileMap = profiles.profiles;
|
|
3428
|
+
const profileMap = /* @__PURE__ */ new Map();
|
|
3429
|
+
for (const [key, profile] of Object.entries(rawProfileMap)) {
|
|
3430
|
+
if (!profile) continue;
|
|
3431
|
+
const normalizedKey = normalizeMemberId(key);
|
|
3432
|
+
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
3433
|
+
profileMap.set(normalizedKey, profile);
|
|
3434
|
+
}
|
|
3435
|
+
const profileId = normalizeMemberId(profile.id);
|
|
3436
|
+
if (profileId && !profileMap.has(profileId)) {
|
|
3437
|
+
profileMap.set(profileId, profile);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3306
3440
|
const rows = ids.map((id) => ({
|
|
3307
3441
|
userId: id,
|
|
3308
|
-
displayName: profileMap
|
|
3309
|
-
zaloName: profileMap
|
|
3442
|
+
displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
|
|
3443
|
+
zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
|
|
3310
3444
|
}));
|
|
3311
3445
|
if (opts.json) {
|
|
3312
3446
|
output(rows, true);
|