@openclaw/zalouser 2026.2.25 → 2026.3.2
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/CHANGELOG.md +22 -0
- package/README.md +41 -147
- package/index.ts +1 -3
- package/package.json +4 -3
- package/src/accounts.test.ts +214 -0
- package/src/accounts.ts +28 -17
- package/src/channel.sendpayload.test.ts +117 -0
- package/src/channel.test.ts +123 -1
- package/src/channel.ts +244 -191
- package/src/config-schema.ts +1 -0
- package/src/group-policy.test.ts +49 -0
- package/src/group-policy.ts +78 -0
- package/src/message-sid.test.ts +66 -0
- package/src/message-sid.ts +80 -0
- package/src/monitor.account-scope.test.ts +123 -0
- package/src/monitor.group-gating.test.ts +216 -0
- package/src/monitor.ts +299 -228
- package/src/onboarding.ts +110 -142
- package/src/probe.test.ts +60 -0
- package/src/probe.ts +19 -12
- package/src/reaction.test.ts +19 -0
- package/src/reaction.ts +29 -0
- package/src/send.test.ts +116 -115
- package/src/send.ts +63 -117
- package/src/status-issues.test.ts +1 -15
- package/src/status-issues.ts +7 -26
- package/src/tool.test.ts +149 -0
- package/src/tool.ts +36 -54
- package/src/types.ts +52 -42
- package/src/zalo-js.ts +1401 -0
- package/src/zca-client.ts +249 -0
- package/src/zca-js-exports.d.ts +22 -0
- package/src/zca.ts +0 -198
package/src/onboarding.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import type {
|
|
2
4
|
ChannelOnboardingAdapter,
|
|
3
5
|
ChannelOnboardingDmPolicy,
|
|
@@ -7,10 +9,12 @@ import type {
|
|
|
7
9
|
import {
|
|
8
10
|
addWildcardAllowFrom,
|
|
9
11
|
DEFAULT_ACCOUNT_ID,
|
|
12
|
+
formatResolvedUnresolvedNote,
|
|
10
13
|
mergeAllowFromEntries,
|
|
11
14
|
normalizeAccountId,
|
|
12
15
|
promptAccountId,
|
|
13
16
|
promptChannelAccessConfig,
|
|
17
|
+
resolvePreferredOpenClawTmpDir,
|
|
14
18
|
} from "openclaw/plugin-sdk";
|
|
15
19
|
import {
|
|
16
20
|
listZalouserAccountIds,
|
|
@@ -18,8 +22,13 @@ import {
|
|
|
18
22
|
resolveZalouserAccountSync,
|
|
19
23
|
checkZcaAuthenticated,
|
|
20
24
|
} from "./accounts.js";
|
|
21
|
-
import
|
|
22
|
-
|
|
25
|
+
import {
|
|
26
|
+
logoutZaloProfile,
|
|
27
|
+
resolveZaloAllowFromEntries,
|
|
28
|
+
resolveZaloGroupsByEntries,
|
|
29
|
+
startZaloQrLogin,
|
|
30
|
+
waitForZaloQrLogin,
|
|
31
|
+
} from "./zalo-js.js";
|
|
23
32
|
|
|
24
33
|
const channel = "zalouser" as const;
|
|
25
34
|
|
|
@@ -86,9 +95,7 @@ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
|
|
|
86
95
|
[
|
|
87
96
|
"Zalo Personal Account login via QR code.",
|
|
88
97
|
"",
|
|
89
|
-
"
|
|
90
|
-
"1) Install zca-cli",
|
|
91
|
-
"2) You'll scan a QR code with your Zalo app",
|
|
98
|
+
"This plugin uses zca-js directly (no external CLI dependency).",
|
|
92
99
|
"",
|
|
93
100
|
"Docs: https://docs.openclaw.ai/channels/zalouser",
|
|
94
101
|
].join("\n"),
|
|
@@ -96,6 +103,25 @@ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
|
|
|
96
103
|
);
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
async function writeQrDataUrlToTempFile(
|
|
107
|
+
qrDataUrl: string,
|
|
108
|
+
profile: string,
|
|
109
|
+
): Promise<string | null> {
|
|
110
|
+
const trimmed = qrDataUrl.trim();
|
|
111
|
+
const match = trimmed.match(/^data:image\/png;base64,(.+)$/i);
|
|
112
|
+
const base64 = (match?.[1] ?? "").trim();
|
|
113
|
+
if (!base64) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const safeProfile = profile.replace(/[^a-zA-Z0-9_-]+/g, "-") || "default";
|
|
117
|
+
const filePath = path.join(
|
|
118
|
+
resolvePreferredOpenClawTmpDir(),
|
|
119
|
+
`openclaw-zalouser-qr-${safeProfile}.png`,
|
|
120
|
+
);
|
|
121
|
+
await fsp.writeFile(filePath, Buffer.from(base64, "base64"));
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
async function promptZalouserAllowFrom(params: {
|
|
100
126
|
cfg: OpenClawConfig;
|
|
101
127
|
prompter: WizardPrompter;
|
|
@@ -110,58 +136,40 @@ async function promptZalouserAllowFrom(params: {
|
|
|
110
136
|
.map((entry) => entry.trim())
|
|
111
137
|
.filter(Boolean);
|
|
112
138
|
|
|
113
|
-
const resolveUserId = async (input: string): Promise<string | null> => {
|
|
114
|
-
const trimmed = input.trim();
|
|
115
|
-
if (!trimmed) {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
if (/^\d+$/.test(trimmed)) {
|
|
119
|
-
return trimmed;
|
|
120
|
-
}
|
|
121
|
-
const ok = await checkZcaInstalled();
|
|
122
|
-
if (!ok) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
const result = await runZca(["friend", "find", trimmed], {
|
|
126
|
-
profile: resolved.profile,
|
|
127
|
-
timeout: 15000,
|
|
128
|
-
});
|
|
129
|
-
if (!result.ok) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
const parsed = parseJsonOutput<ZcaFriend[]>(result.stdout);
|
|
133
|
-
const rows = Array.isArray(parsed) ? parsed : [];
|
|
134
|
-
const match = rows[0];
|
|
135
|
-
if (!match?.userId) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (rows.length > 1) {
|
|
139
|
-
await prompter.note(
|
|
140
|
-
`Multiple matches for "${trimmed}", using ${match.displayName ?? match.userId}.`,
|
|
141
|
-
"Zalo Personal allowlist",
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return String(match.userId);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
139
|
while (true) {
|
|
148
140
|
const entry = await prompter.text({
|
|
149
|
-
message: "Zalouser allowFrom (
|
|
141
|
+
message: "Zalouser allowFrom (name or user id)",
|
|
150
142
|
placeholder: "Alice, 123456789",
|
|
151
143
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
152
144
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
153
145
|
});
|
|
154
146
|
const parts = parseInput(String(entry));
|
|
155
|
-
const
|
|
156
|
-
|
|
147
|
+
const resolvedEntries = await resolveZaloAllowFromEntries({
|
|
148
|
+
profile: resolved.profile,
|
|
149
|
+
entries: parts,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const unresolved = resolvedEntries.filter((item) => !item.resolved).map((item) => item.input);
|
|
157
153
|
if (unresolved.length > 0) {
|
|
158
154
|
await prompter.note(
|
|
159
|
-
`Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or
|
|
155
|
+
`Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or exact friend names.`,
|
|
160
156
|
"Zalo Personal allowlist",
|
|
161
157
|
);
|
|
162
158
|
continue;
|
|
163
159
|
}
|
|
164
|
-
|
|
160
|
+
|
|
161
|
+
const resolvedIds = resolvedEntries
|
|
162
|
+
.filter((item) => item.resolved && item.id)
|
|
163
|
+
.map((item) => item.id as string);
|
|
164
|
+
const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
|
|
165
|
+
|
|
166
|
+
const notes = resolvedEntries
|
|
167
|
+
.filter((item) => item.note)
|
|
168
|
+
.map((item) => `${item.input} -> ${item.id} (${item.note})`);
|
|
169
|
+
if (notes.length > 0) {
|
|
170
|
+
await prompter.note(notes.join("\n"), "Zalo Personal allowlist");
|
|
171
|
+
}
|
|
172
|
+
|
|
165
173
|
return setZalouserAccountScopedConfig(cfg, accountId, {
|
|
166
174
|
dmPolicy: "allowlist",
|
|
167
175
|
allowFrom: unique,
|
|
@@ -190,49 +198,6 @@ function setZalouserGroupAllowlist(
|
|
|
190
198
|
});
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
async function resolveZalouserGroups(params: {
|
|
194
|
-
cfg: OpenClawConfig;
|
|
195
|
-
accountId: string;
|
|
196
|
-
entries: string[];
|
|
197
|
-
}): Promise<Array<{ input: string; resolved: boolean; id?: string }>> {
|
|
198
|
-
const account = resolveZalouserAccountSync({ cfg: params.cfg, accountId: params.accountId });
|
|
199
|
-
const result = await runZca(["group", "list", "-j"], {
|
|
200
|
-
profile: account.profile,
|
|
201
|
-
timeout: 15000,
|
|
202
|
-
});
|
|
203
|
-
if (!result.ok) {
|
|
204
|
-
throw new Error(result.stderr || "Failed to list groups");
|
|
205
|
-
}
|
|
206
|
-
const groups = (parseJsonOutput<ZcaGroup[]>(result.stdout) ?? []).filter((group) =>
|
|
207
|
-
Boolean(group.groupId),
|
|
208
|
-
);
|
|
209
|
-
const byName = new Map<string, ZcaGroup[]>();
|
|
210
|
-
for (const group of groups) {
|
|
211
|
-
const name = group.name?.trim().toLowerCase();
|
|
212
|
-
if (!name) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const list = byName.get(name) ?? [];
|
|
216
|
-
list.push(group);
|
|
217
|
-
byName.set(name, list);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return params.entries.map((input) => {
|
|
221
|
-
const trimmed = input.trim();
|
|
222
|
-
if (!trimmed) {
|
|
223
|
-
return { input, resolved: false };
|
|
224
|
-
}
|
|
225
|
-
if (/^\d+$/.test(trimmed)) {
|
|
226
|
-
return { input, resolved: true, id: trimmed };
|
|
227
|
-
}
|
|
228
|
-
const matches = byName.get(trimmed.toLowerCase()) ?? [];
|
|
229
|
-
const match = matches[0];
|
|
230
|
-
return match?.groupId
|
|
231
|
-
? { input, resolved: true, id: String(match.groupId) }
|
|
232
|
-
: { input, resolved: false };
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
201
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
237
202
|
label: "Zalo Personal",
|
|
238
203
|
channel,
|
|
@@ -246,7 +211,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
246
211
|
? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
|
|
247
212
|
: resolveDefaultZalouserAccountId(cfg);
|
|
248
213
|
return promptZalouserAllowFrom({
|
|
249
|
-
cfg
|
|
214
|
+
cfg,
|
|
250
215
|
prompter,
|
|
251
216
|
accountId: id,
|
|
252
217
|
});
|
|
@@ -260,7 +225,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
260
225
|
const ids = listZalouserAccountIds(cfg);
|
|
261
226
|
let configured = false;
|
|
262
227
|
for (const accountId of ids) {
|
|
263
|
-
const account = resolveZalouserAccountSync({ cfg
|
|
228
|
+
const account = resolveZalouserAccountSync({ cfg, accountId });
|
|
264
229
|
const isAuth = await checkZcaAuthenticated(account.profile);
|
|
265
230
|
if (isAuth) {
|
|
266
231
|
configured = true;
|
|
@@ -282,28 +247,13 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
282
247
|
shouldPromptAccountIds,
|
|
283
248
|
forceAllowFrom,
|
|
284
249
|
}) => {
|
|
285
|
-
// Check zca is installed
|
|
286
|
-
const zcaInstalled = await checkZcaInstalled();
|
|
287
|
-
if (!zcaInstalled) {
|
|
288
|
-
await prompter.note(
|
|
289
|
-
[
|
|
290
|
-
"The `zca` binary was not found in PATH.",
|
|
291
|
-
"",
|
|
292
|
-
"Install zca-cli, then re-run onboarding:",
|
|
293
|
-
"Docs: https://docs.openclaw.ai/channels/zalouser",
|
|
294
|
-
].join("\n"),
|
|
295
|
-
"Missing Dependency",
|
|
296
|
-
);
|
|
297
|
-
return { cfg, accountId: DEFAULT_ACCOUNT_ID };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
250
|
const zalouserOverride = accountOverrides.zalouser?.trim();
|
|
301
251
|
const defaultAccountId = resolveDefaultZalouserAccountId(cfg);
|
|
302
252
|
let accountId = zalouserOverride ? normalizeAccountId(zalouserOverride) : defaultAccountId;
|
|
303
253
|
|
|
304
254
|
if (shouldPromptAccountIds && !zalouserOverride) {
|
|
305
255
|
accountId = await promptAccountId({
|
|
306
|
-
cfg
|
|
256
|
+
cfg,
|
|
307
257
|
prompter,
|
|
308
258
|
label: "Zalo Personal",
|
|
309
259
|
currentId: accountId,
|
|
@@ -325,23 +275,32 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
325
275
|
});
|
|
326
276
|
|
|
327
277
|
if (wantsLogin) {
|
|
328
|
-
await
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
278
|
+
const start = await startZaloQrLogin({ profile: account.profile, timeoutMs: 35_000 });
|
|
279
|
+
if (start.qrDataUrl) {
|
|
280
|
+
const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
|
|
281
|
+
await prompter.note(
|
|
282
|
+
[
|
|
283
|
+
start.message,
|
|
284
|
+
qrPath
|
|
285
|
+
? `QR image saved to: ${qrPath}`
|
|
286
|
+
: "Could not write QR image file; use gateway web login UI instead.",
|
|
287
|
+
"Scan + approve on phone, then continue.",
|
|
288
|
+
].join("\n"),
|
|
289
|
+
"QR Login",
|
|
290
|
+
);
|
|
291
|
+
const scanned = await prompter.confirm({
|
|
292
|
+
message: "Did you scan and approve the QR on your phone?",
|
|
293
|
+
initialValue: true,
|
|
294
|
+
});
|
|
295
|
+
if (scanned) {
|
|
296
|
+
const waited = await waitForZaloQrLogin({
|
|
297
|
+
profile: account.profile,
|
|
298
|
+
timeoutMs: 120_000,
|
|
299
|
+
});
|
|
300
|
+
await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
|
|
344
301
|
}
|
|
302
|
+
} else {
|
|
303
|
+
await prompter.note(start.message, "Login pending");
|
|
345
304
|
}
|
|
346
305
|
}
|
|
347
306
|
} else {
|
|
@@ -350,12 +309,26 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
350
309
|
initialValue: true,
|
|
351
310
|
});
|
|
352
311
|
if (!keepSession) {
|
|
353
|
-
await
|
|
354
|
-
await
|
|
312
|
+
await logoutZaloProfile(account.profile);
|
|
313
|
+
const start = await startZaloQrLogin({
|
|
314
|
+
profile: account.profile,
|
|
315
|
+
force: true,
|
|
316
|
+
timeoutMs: 35_000,
|
|
317
|
+
});
|
|
318
|
+
if (start.qrDataUrl) {
|
|
319
|
+
const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
|
|
320
|
+
await prompter.note(
|
|
321
|
+
[start.message, qrPath ? `QR image saved to: ${qrPath}` : undefined]
|
|
322
|
+
.filter(Boolean)
|
|
323
|
+
.join("\n"),
|
|
324
|
+
"QR Login",
|
|
325
|
+
);
|
|
326
|
+
const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 120_000 });
|
|
327
|
+
await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
|
|
328
|
+
}
|
|
355
329
|
}
|
|
356
330
|
}
|
|
357
331
|
|
|
358
|
-
// Enable the channel
|
|
359
332
|
next = setZalouserAccountScopedConfig(
|
|
360
333
|
next,
|
|
361
334
|
accountId,
|
|
@@ -371,14 +344,16 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
371
344
|
});
|
|
372
345
|
}
|
|
373
346
|
|
|
347
|
+
const updatedAccount = resolveZalouserAccountSync({ cfg: next, accountId });
|
|
374
348
|
const accessConfig = await promptChannelAccessConfig({
|
|
375
349
|
prompter,
|
|
376
350
|
label: "Zalo groups",
|
|
377
|
-
currentPolicy:
|
|
378
|
-
currentEntries: Object.keys(
|
|
351
|
+
currentPolicy: updatedAccount.config.groupPolicy ?? "allowlist",
|
|
352
|
+
currentEntries: Object.keys(updatedAccount.config.groups ?? {}),
|
|
379
353
|
placeholder: "Family, Work, 123456789",
|
|
380
|
-
updatePrompt: Boolean(
|
|
354
|
+
updatePrompt: Boolean(updatedAccount.config.groups),
|
|
381
355
|
});
|
|
356
|
+
|
|
382
357
|
if (accessConfig) {
|
|
383
358
|
if (accessConfig.policy !== "allowlist") {
|
|
384
359
|
next = setZalouserGroupPolicy(next, accountId, accessConfig.policy);
|
|
@@ -386,9 +361,8 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
386
361
|
let keys = accessConfig.entries;
|
|
387
362
|
if (accessConfig.entries.length > 0) {
|
|
388
363
|
try {
|
|
389
|
-
const resolved = await
|
|
390
|
-
|
|
391
|
-
accountId,
|
|
364
|
+
const resolved = await resolveZaloGroupsByEntries({
|
|
365
|
+
profile: updatedAccount.profile,
|
|
392
366
|
entries: accessConfig.entries,
|
|
393
367
|
});
|
|
394
368
|
const resolvedIds = resolved
|
|
@@ -398,18 +372,12 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
398
372
|
.filter((entry) => !entry.resolved)
|
|
399
373
|
.map((entry) => entry.input);
|
|
400
374
|
keys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
: undefined,
|
|
408
|
-
]
|
|
409
|
-
.filter(Boolean)
|
|
410
|
-
.join("\n"),
|
|
411
|
-
"Zalo groups",
|
|
412
|
-
);
|
|
375
|
+
const resolution = formatResolvedUnresolvedNote({
|
|
376
|
+
resolved: resolvedIds,
|
|
377
|
+
unresolved,
|
|
378
|
+
});
|
|
379
|
+
if (resolution) {
|
|
380
|
+
await prompter.note(resolution, "Zalo groups");
|
|
413
381
|
}
|
|
414
382
|
} catch (err) {
|
|
415
383
|
await prompter.note(
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { probeZalouser } from "./probe.js";
|
|
3
|
+
import { getZaloUserInfo } from "./zalo-js.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./zalo-js.js", () => ({
|
|
6
|
+
getZaloUserInfo: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const mockGetUserInfo = vi.mocked(getZaloUserInfo);
|
|
10
|
+
|
|
11
|
+
describe("probeZalouser", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockGetUserInfo.mockReset();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns ok=true with user when authenticated", async () => {
|
|
21
|
+
mockGetUserInfo.mockResolvedValueOnce({
|
|
22
|
+
userId: "123",
|
|
23
|
+
displayName: "Alice",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await expect(probeZalouser("default")).resolves.toEqual({
|
|
27
|
+
ok: true,
|
|
28
|
+
user: { userId: "123", displayName: "Alice" },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns not authenticated when no user info is returned", async () => {
|
|
33
|
+
mockGetUserInfo.mockResolvedValueOnce(null);
|
|
34
|
+
await expect(probeZalouser("default")).resolves.toEqual({
|
|
35
|
+
ok: false,
|
|
36
|
+
error: "Not authenticated",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns error when user lookup throws", async () => {
|
|
41
|
+
mockGetUserInfo.mockRejectedValueOnce(new Error("network down"));
|
|
42
|
+
await expect(probeZalouser("default")).resolves.toEqual({
|
|
43
|
+
ok: false,
|
|
44
|
+
error: "network down",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("times out when lookup takes too long", async () => {
|
|
49
|
+
vi.useFakeTimers();
|
|
50
|
+
mockGetUserInfo.mockReturnValueOnce(new Promise(() => undefined));
|
|
51
|
+
|
|
52
|
+
const pending = probeZalouser("default", 10);
|
|
53
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
54
|
+
|
|
55
|
+
await expect(pending).resolves.toEqual({
|
|
56
|
+
ok: false,
|
|
57
|
+
error: "Not authenticated",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/probe.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BaseProbeResult } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { ZcaUserInfo } from "./types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getZaloUserInfo } from "./zalo-js.js";
|
|
4
4
|
|
|
5
5
|
export type ZalouserProbeResult = BaseProbeResult<string> & {
|
|
6
6
|
user?: ZcaUserInfo;
|
|
@@ -10,18 +10,25 @@ export async function probeZalouser(
|
|
|
10
10
|
profile: string,
|
|
11
11
|
timeoutMs?: number,
|
|
12
12
|
): Promise<ZalouserProbeResult> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
try {
|
|
14
|
+
const user = timeoutMs
|
|
15
|
+
? await Promise.race([
|
|
16
|
+
getZaloUserInfo(profile),
|
|
17
|
+
new Promise<null>((resolve) =>
|
|
18
|
+
setTimeout(() => resolve(null), Math.max(timeoutMs, 1000)),
|
|
19
|
+
),
|
|
20
|
+
])
|
|
21
|
+
: await getZaloUserInfo(profile);
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
if (!user) {
|
|
24
|
+
return { ok: false, error: "Not authenticated" };
|
|
25
|
+
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return {
|
|
27
|
+
return { ok: true, user };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: error instanceof Error ? error.message : String(error),
|
|
32
|
+
};
|
|
25
33
|
}
|
|
26
|
-
return { ok: true, user };
|
|
27
34
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeZaloReactionIcon } from "./reaction.js";
|
|
3
|
+
|
|
4
|
+
describe("zalouser reaction alias normalization", () => {
|
|
5
|
+
it("maps common aliases", () => {
|
|
6
|
+
expect(normalizeZaloReactionIcon("like")).toBe("/-strong");
|
|
7
|
+
expect(normalizeZaloReactionIcon("👍")).toBe("/-strong");
|
|
8
|
+
expect(normalizeZaloReactionIcon("heart")).toBe("/-heart");
|
|
9
|
+
expect(normalizeZaloReactionIcon("😂")).toBe(":>");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("defaults empty icon to like", () => {
|
|
13
|
+
expect(normalizeZaloReactionIcon("")).toBe("/-strong");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("passes through unknown custom reactions", () => {
|
|
17
|
+
expect(normalizeZaloReactionIcon("/custom")).toBe("/custom");
|
|
18
|
+
});
|
|
19
|
+
});
|
package/src/reaction.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Reactions } from "./zca-client.js";
|
|
2
|
+
|
|
3
|
+
const REACTION_ALIAS_MAP = new Map<string, string>([
|
|
4
|
+
["like", Reactions.LIKE],
|
|
5
|
+
["👍", Reactions.LIKE],
|
|
6
|
+
[":+1:", Reactions.LIKE],
|
|
7
|
+
["heart", Reactions.HEART],
|
|
8
|
+
["❤️", Reactions.HEART],
|
|
9
|
+
["<3", Reactions.HEART],
|
|
10
|
+
["haha", Reactions.HAHA],
|
|
11
|
+
["laugh", Reactions.HAHA],
|
|
12
|
+
["😂", Reactions.HAHA],
|
|
13
|
+
["wow", Reactions.WOW],
|
|
14
|
+
["😮", Reactions.WOW],
|
|
15
|
+
["cry", Reactions.CRY],
|
|
16
|
+
["😢", Reactions.CRY],
|
|
17
|
+
["angry", Reactions.ANGRY],
|
|
18
|
+
["😡", Reactions.ANGRY],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function normalizeZaloReactionIcon(raw: string): string {
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
return Reactions.LIKE;
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
REACTION_ALIAS_MAP.get(trimmed.toLowerCase()) ?? REACTION_ALIAS_MAP.get(trimmed) ?? trimmed
|
|
28
|
+
);
|
|
29
|
+
}
|