openzca 0.1.16 → 0.1.18
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 +3 -8
- package/dist/cli.js +242 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
# openzca
|
|
2
2
|
|
|
3
|
-
Free and open-source CLI for Zalo, built on [zca-js](https://github.com/
|
|
3
|
+
Free and open-source CLI for Zalo, built on [zca-js](https://github.com/RFS-ADRENO/zca-js). Command structure compatible with [zca-cli.dev/docs](https://zca-cli.dev/docs).
|
|
4
4
|
|
|
5
|
-
## Integrate with OpenClaw
|
|
6
|
-
|
|
7
|
-
Prompt:
|
|
8
|
-
```
|
|
9
|
-
help me to enable zalouser openclaw plugin. However, don't install zca cli follow their instruction, please use: npm install -g openzca@latest
|
|
10
|
-
```
|
|
5
|
+
## Integrate with OpenClaw OpenZalo plugin (including legacy `zalouser`)
|
|
11
6
|
|
|
12
7
|
|
|
13
8
|
## Install
|
|
@@ -296,7 +291,7 @@ Supervised mode notes:
|
|
|
296
291
|
|
|
297
292
|
## Multi-account profiles
|
|
298
293
|
|
|
299
|
-
Use `--profile <name>` or set `ZCA_PROFILE=<name>` to switch between accounts. Manage profiles with the `account` commands.
|
|
294
|
+
Use `--profile <name>` or set `OPENZCA_PROFILE=<name>` (or legacy `ZCA_PROFILE=<name>`) to switch between accounts. Manage profiles with the `account` commands.
|
|
300
295
|
|
|
301
296
|
Profile data is stored in `~/.openzca/` (override with `OPENZCA_HOME`):
|
|
302
297
|
|
package/dist/cli.js
CHANGED
|
@@ -164,7 +164,7 @@ async function removeProfile(name) {
|
|
|
164
164
|
}
|
|
165
165
|
async function resolveProfileName(flagProfile) {
|
|
166
166
|
const db = await ensureProfilesDb();
|
|
167
|
-
const picked = flagProfile && flagProfile.trim() || process.env.
|
|
167
|
+
const picked = flagProfile && flagProfile.trim() || (process.env.OPENZCA_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim()) || db.defaultProfile || DEFAULT_PROFILE;
|
|
168
168
|
if (!db.profiles[picked]) {
|
|
169
169
|
if (picked === DEFAULT_PROFILE) {
|
|
170
170
|
await ensureProfile(DEFAULT_PROFILE);
|
|
@@ -765,7 +765,7 @@ async function currentProfile(_command) {
|
|
|
765
765
|
async function profileForLogin() {
|
|
766
766
|
const opts = program.opts();
|
|
767
767
|
const explicit = opts.profile?.trim();
|
|
768
|
-
const fromEnv = process.env.ZCA_PROFILE?.trim();
|
|
768
|
+
const fromEnv = process.env.OPENZCA_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim();
|
|
769
769
|
if (explicit) {
|
|
770
770
|
await ensureProfile(explicit);
|
|
771
771
|
return explicit;
|
|
@@ -806,6 +806,139 @@ async function refreshCacheForProfile(profile, api) {
|
|
|
806
806
|
groups: groups.length
|
|
807
807
|
};
|
|
808
808
|
}
|
|
809
|
+
function parsePositiveIntFromEnv(name, fallback) {
|
|
810
|
+
const raw = process.env[name]?.trim();
|
|
811
|
+
if (!raw) return fallback;
|
|
812
|
+
const parsed = Number.parseInt(raw, 10);
|
|
813
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
814
|
+
return parsed;
|
|
815
|
+
}
|
|
816
|
+
function isListenerAlreadyStarted(error) {
|
|
817
|
+
if (!(error instanceof Error)) return false;
|
|
818
|
+
return /already started/i.test(error.message);
|
|
819
|
+
}
|
|
820
|
+
function toErrorText(error) {
|
|
821
|
+
return error instanceof Error ? error.message : String(error);
|
|
822
|
+
}
|
|
823
|
+
async function withTimeout(task, timeoutMs, message) {
|
|
824
|
+
let timeoutId;
|
|
825
|
+
try {
|
|
826
|
+
const timeout = new Promise((_, reject) => {
|
|
827
|
+
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
828
|
+
});
|
|
829
|
+
return await Promise.race([task, timeout]);
|
|
830
|
+
} finally {
|
|
831
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function withUploadListener(api, command, task) {
|
|
835
|
+
const connectTimeoutMs = parsePositiveIntFromEnv(
|
|
836
|
+
"OPENZCA_UPLOAD_LISTENER_CONNECT_TIMEOUT_MS",
|
|
837
|
+
8e3
|
|
838
|
+
);
|
|
839
|
+
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
840
|
+
"OPENZCA_UPLOAD_TIMEOUT_MS",
|
|
841
|
+
12e4
|
|
842
|
+
);
|
|
843
|
+
let startedHere = false;
|
|
844
|
+
const sinkError = (error) => {
|
|
845
|
+
writeDebugLine(
|
|
846
|
+
"msg.upload.listener.error",
|
|
847
|
+
{
|
|
848
|
+
message: toErrorText(error)
|
|
849
|
+
},
|
|
850
|
+
command
|
|
851
|
+
);
|
|
852
|
+
};
|
|
853
|
+
const sinkClosed = (code, reason) => {
|
|
854
|
+
writeDebugLine(
|
|
855
|
+
"msg.upload.listener.closed",
|
|
856
|
+
{
|
|
857
|
+
code,
|
|
858
|
+
reason: reason || void 0
|
|
859
|
+
},
|
|
860
|
+
command
|
|
861
|
+
);
|
|
862
|
+
};
|
|
863
|
+
api.listener.on("error", sinkError);
|
|
864
|
+
api.listener.on("closed", sinkClosed);
|
|
865
|
+
try {
|
|
866
|
+
await new Promise((resolve, reject) => {
|
|
867
|
+
let settled = false;
|
|
868
|
+
let timeoutId;
|
|
869
|
+
const cleanup = () => {
|
|
870
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
871
|
+
api.listener.off("connected", onConnected);
|
|
872
|
+
api.listener.off("error", onConnectError);
|
|
873
|
+
api.listener.off("closed", onConnectClosed);
|
|
874
|
+
};
|
|
875
|
+
const finish = (error) => {
|
|
876
|
+
if (settled) return;
|
|
877
|
+
settled = true;
|
|
878
|
+
cleanup();
|
|
879
|
+
if (error) {
|
|
880
|
+
reject(error);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
resolve();
|
|
884
|
+
};
|
|
885
|
+
const onConnected = () => {
|
|
886
|
+
writeDebugLine("msg.upload.listener.connected", void 0, command);
|
|
887
|
+
finish();
|
|
888
|
+
};
|
|
889
|
+
const onConnectError = (error) => {
|
|
890
|
+
finish(new Error(`Upload listener connection error: ${toErrorText(error)}`));
|
|
891
|
+
};
|
|
892
|
+
const onConnectClosed = (code, reason) => {
|
|
893
|
+
finish(
|
|
894
|
+
new Error(
|
|
895
|
+
`Upload listener closed before ready (code=${code}${reason ? `, reason=${reason}` : ""}).`
|
|
896
|
+
)
|
|
897
|
+
);
|
|
898
|
+
};
|
|
899
|
+
timeoutId = setTimeout(() => {
|
|
900
|
+
finish(new Error(`Timed out waiting ${connectTimeoutMs}ms for upload listener connection.`));
|
|
901
|
+
}, connectTimeoutMs);
|
|
902
|
+
api.listener.on("connected", onConnected);
|
|
903
|
+
api.listener.on("error", onConnectError);
|
|
904
|
+
api.listener.on("closed", onConnectClosed);
|
|
905
|
+
try {
|
|
906
|
+
api.listener.start();
|
|
907
|
+
startedHere = true;
|
|
908
|
+
writeDebugLine(
|
|
909
|
+
"msg.upload.listener.start",
|
|
910
|
+
{
|
|
911
|
+
connectTimeoutMs,
|
|
912
|
+
uploadTimeoutMs
|
|
913
|
+
},
|
|
914
|
+
command
|
|
915
|
+
);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (isListenerAlreadyStarted(error)) {
|
|
918
|
+
writeDebugLine("msg.upload.listener.already_started", void 0, command);
|
|
919
|
+
finish();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
finish(error);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
return await withTimeout(
|
|
926
|
+
task(),
|
|
927
|
+
uploadTimeoutMs,
|
|
928
|
+
`Timed out waiting ${uploadTimeoutMs}ms for file upload completion.`
|
|
929
|
+
);
|
|
930
|
+
} finally {
|
|
931
|
+
api.listener.off("error", sinkError);
|
|
932
|
+
api.listener.off("closed", sinkClosed);
|
|
933
|
+
if (startedHere) {
|
|
934
|
+
try {
|
|
935
|
+
api.listener.stop();
|
|
936
|
+
writeDebugLine("msg.upload.listener.stop", void 0, command);
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
809
942
|
async function fetchRecentMessagesViaListener(api, threadId, threadType, count) {
|
|
810
943
|
return new Promise((resolve, reject) => {
|
|
811
944
|
let settled = false;
|
|
@@ -1653,7 +1786,7 @@ program.hook("preAction", (_parent, actionCommand) => {
|
|
|
1653
1786
|
argv: process.argv.slice(2),
|
|
1654
1787
|
cwd: process.cwd(),
|
|
1655
1788
|
profileFlag: getDebugOptions(actionCommand).profile ?? null,
|
|
1656
|
-
envProfile: process.env.ZCA_PROFILE ?? null
|
|
1789
|
+
envProfile: process.env.OPENZCA_PROFILE ?? process.env.ZCA_PROFILE ?? null
|
|
1657
1790
|
},
|
|
1658
1791
|
actionCommand
|
|
1659
1792
|
);
|
|
@@ -2082,6 +2215,32 @@ msg.command("undo <msgId> <cliMsgId> <threadId>").option("-g, --group", "Undo in
|
|
|
2082
2215
|
}
|
|
2083
2216
|
)
|
|
2084
2217
|
);
|
|
2218
|
+
msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group", "Edit in group").description("Edit message (compatibility shim: recall old message then resend new text)").action(
|
|
2219
|
+
wrapAction(
|
|
2220
|
+
async (msgId, cliMsgId, threadId, message, opts, command) => {
|
|
2221
|
+
const { api } = await requireApi(command);
|
|
2222
|
+
const type = asThreadType(opts.group);
|
|
2223
|
+
const undoResponse = await api.undo(
|
|
2224
|
+
{
|
|
2225
|
+
msgId,
|
|
2226
|
+
cliMsgId
|
|
2227
|
+
},
|
|
2228
|
+
threadId,
|
|
2229
|
+
type
|
|
2230
|
+
);
|
|
2231
|
+
const sendResponse = await api.sendMessage(message, threadId, type);
|
|
2232
|
+
output(
|
|
2233
|
+
{
|
|
2234
|
+
mode: "undo+send",
|
|
2235
|
+
nativeEditSupported: false,
|
|
2236
|
+
undo: undoResponse,
|
|
2237
|
+
send: sendResponse
|
|
2238
|
+
},
|
|
2239
|
+
false
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
)
|
|
2243
|
+
);
|
|
2085
2244
|
msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeatable)", collectValues, []).option("-g, --group", "Upload in group").description("Upload and send file(s)").action(
|
|
2086
2245
|
wrapAction(
|
|
2087
2246
|
async (arg1, arg2, opts, command) => {
|
|
@@ -2111,13 +2270,17 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
2111
2270
|
);
|
|
2112
2271
|
}
|
|
2113
2272
|
await assertFilesExist(attachments);
|
|
2114
|
-
const response = await
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2273
|
+
const response = await withUploadListener(
|
|
2274
|
+
api,
|
|
2275
|
+
command,
|
|
2276
|
+
async () => api.sendMessage(
|
|
2277
|
+
{
|
|
2278
|
+
msg: "",
|
|
2279
|
+
attachments
|
|
2280
|
+
},
|
|
2281
|
+
threadId,
|
|
2282
|
+
asThreadType(opts.group)
|
|
2283
|
+
)
|
|
2121
2284
|
);
|
|
2122
2285
|
output(response, false);
|
|
2123
2286
|
} finally {
|
|
@@ -2164,6 +2327,75 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
2164
2327
|
}
|
|
2165
2328
|
)
|
|
2166
2329
|
);
|
|
2330
|
+
msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").description("Pin conversation").action(
|
|
2331
|
+
wrapAction(async (threadId, opts, command) => {
|
|
2332
|
+
const { api } = await requireApi(command);
|
|
2333
|
+
const type = asThreadType(opts.group);
|
|
2334
|
+
const response = await api.setPinnedConversations(true, threadId, type);
|
|
2335
|
+
output(
|
|
2336
|
+
{
|
|
2337
|
+
threadId,
|
|
2338
|
+
threadType: type === ThreadType.Group ? "group" : "user",
|
|
2339
|
+
pinned: true,
|
|
2340
|
+
response
|
|
2341
|
+
},
|
|
2342
|
+
false
|
|
2343
|
+
);
|
|
2344
|
+
})
|
|
2345
|
+
);
|
|
2346
|
+
msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation").description("Unpin conversation").action(
|
|
2347
|
+
wrapAction(async (threadId, opts, command) => {
|
|
2348
|
+
const { api } = await requireApi(command);
|
|
2349
|
+
const type = asThreadType(opts.group);
|
|
2350
|
+
const response = await api.setPinnedConversations(false, threadId, type);
|
|
2351
|
+
output(
|
|
2352
|
+
{
|
|
2353
|
+
threadId,
|
|
2354
|
+
threadType: type === ThreadType.Group ? "group" : "user",
|
|
2355
|
+
pinned: false,
|
|
2356
|
+
response
|
|
2357
|
+
},
|
|
2358
|
+
false
|
|
2359
|
+
);
|
|
2360
|
+
})
|
|
2361
|
+
);
|
|
2362
|
+
msg.command("list-pins").option("-j, --json", "JSON output").description("List pinned conversations").action(
|
|
2363
|
+
wrapAction(async (opts, command) => {
|
|
2364
|
+
const { api } = await requireApi(command);
|
|
2365
|
+
const response = await api.getPinConversations();
|
|
2366
|
+
if (opts.json) {
|
|
2367
|
+
output(response, true);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
output(
|
|
2371
|
+
response.conversations.map((threadId) => ({
|
|
2372
|
+
threadId,
|
|
2373
|
+
pinned: true
|
|
2374
|
+
})),
|
|
2375
|
+
false
|
|
2376
|
+
);
|
|
2377
|
+
})
|
|
2378
|
+
);
|
|
2379
|
+
msg.command("member-info <userId>").option("-j, --json", "JSON output").description("Get member/user profile info").action(
|
|
2380
|
+
wrapAction(async (userId, opts, command) => {
|
|
2381
|
+
const { api } = await requireApi(command);
|
|
2382
|
+
const response = await api.getUserInfo(userId);
|
|
2383
|
+
if (opts.json) {
|
|
2384
|
+
output(response, true);
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
const profiles = response.changed_profiles ?? {};
|
|
2388
|
+
const matchedProfile = profiles[userId] ?? profiles[`${userId}_0`] ?? Object.values(profiles)[0] ?? null;
|
|
2389
|
+
output(
|
|
2390
|
+
{
|
|
2391
|
+
userId,
|
|
2392
|
+
found: Boolean(matchedProfile),
|
|
2393
|
+
profile: matchedProfile
|
|
2394
|
+
},
|
|
2395
|
+
false
|
|
2396
|
+
);
|
|
2397
|
+
})
|
|
2398
|
+
);
|
|
2167
2399
|
var group = program.command("group").description("Group management");
|
|
2168
2400
|
group.command("list").option("-j, --json", "JSON output").description("List groups").action(
|
|
2169
2401
|
wrapAction(async (opts, command) => {
|