@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/channel.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import type {
|
|
2
4
|
ChannelAccountSnapshot,
|
|
3
5
|
ChannelDirectoryEntry,
|
|
4
6
|
ChannelDock,
|
|
5
7
|
ChannelGroupContext,
|
|
8
|
+
ChannelMessageActionAdapter,
|
|
6
9
|
ChannelPlugin,
|
|
7
10
|
OpenClawConfig,
|
|
8
11
|
GroupToolPolicyConfig,
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
formatPairingApproveHint,
|
|
18
21
|
migrateBaseNameToDefaultAccount,
|
|
19
22
|
normalizeAccountId,
|
|
23
|
+
resolvePreferredOpenClawTmpDir,
|
|
20
24
|
resolveChannelAccountConfigBasePath,
|
|
21
25
|
setAccountEnabledInConfigSection,
|
|
22
26
|
} from "openclaw/plugin-sdk";
|
|
@@ -29,12 +33,21 @@ import {
|
|
|
29
33
|
type ResolvedZalouserAccount,
|
|
30
34
|
} from "./accounts.js";
|
|
31
35
|
import { ZalouserConfigSchema } from "./config-schema.js";
|
|
36
|
+
import { buildZalouserGroupCandidates, findZalouserGroupEntry } from "./group-policy.js";
|
|
37
|
+
import { resolveZalouserReactionMessageIds } from "./message-sid.js";
|
|
32
38
|
import { zalouserOnboardingAdapter } from "./onboarding.js";
|
|
33
39
|
import { probeZalouser } from "./probe.js";
|
|
34
|
-
import { sendMessageZalouser } from "./send.js";
|
|
40
|
+
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
|
|
35
41
|
import { collectZalouserStatusIssues } from "./status-issues.js";
|
|
36
|
-
import
|
|
37
|
-
|
|
42
|
+
import {
|
|
43
|
+
listZaloFriendsMatching,
|
|
44
|
+
listZaloGroupMembers,
|
|
45
|
+
listZaloGroupsMatching,
|
|
46
|
+
logoutZaloProfile,
|
|
47
|
+
startZaloQrLogin,
|
|
48
|
+
waitForZaloQrLogin,
|
|
49
|
+
getZaloUserInfo,
|
|
50
|
+
} from "./zalo-js.js";
|
|
38
51
|
|
|
39
52
|
const meta = {
|
|
40
53
|
id: "zalouser",
|
|
@@ -51,11 +64,30 @@ const meta = {
|
|
|
51
64
|
function resolveZalouserQrProfile(accountId?: string | null): string {
|
|
52
65
|
const normalized = normalizeAccountId(accountId);
|
|
53
66
|
if (!normalized || normalized === DEFAULT_ACCOUNT_ID) {
|
|
54
|
-
return process.env.ZCA_PROFILE?.trim() || "default";
|
|
67
|
+
return process.env.ZALOUSER_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim() || "default";
|
|
55
68
|
}
|
|
56
69
|
return normalized;
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
async function writeQrDataUrlToTempFile(
|
|
73
|
+
qrDataUrl: string,
|
|
74
|
+
profile: string,
|
|
75
|
+
): Promise<string | null> {
|
|
76
|
+
const trimmed = qrDataUrl.trim();
|
|
77
|
+
const match = trimmed.match(/^data:image\/png;base64,(.+)$/i);
|
|
78
|
+
const base64 = (match?.[1] ?? "").trim();
|
|
79
|
+
if (!base64) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const safeProfile = profile.replace(/[^a-zA-Z0-9_-]+/g, "-") || "default";
|
|
83
|
+
const filePath = path.join(
|
|
84
|
+
resolvePreferredOpenClawTmpDir(),
|
|
85
|
+
`openclaw-zalouser-qr-${safeProfile}.png`,
|
|
86
|
+
);
|
|
87
|
+
await fsp.writeFile(filePath, Buffer.from(base64, "base64"));
|
|
88
|
+
return filePath;
|
|
89
|
+
}
|
|
90
|
+
|
|
59
91
|
function mapUser(params: {
|
|
60
92
|
id: string;
|
|
61
93
|
name?: string | null;
|
|
@@ -92,20 +124,106 @@ function resolveZalouserGroupToolPolicy(
|
|
|
92
124
|
accountId: params.accountId ?? undefined,
|
|
93
125
|
});
|
|
94
126
|
const groups = account.config.groups ?? {};
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
const entry = findZalouserGroupEntry(
|
|
128
|
+
groups,
|
|
129
|
+
buildZalouserGroupCandidates({
|
|
130
|
+
groupId: params.groupId,
|
|
131
|
+
groupChannel: params.groupChannel,
|
|
132
|
+
includeWildcard: true,
|
|
133
|
+
}),
|
|
99
134
|
);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
return entry?.tools;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
|
|
139
|
+
const account = resolveZalouserAccountSync({
|
|
140
|
+
cfg: params.cfg,
|
|
141
|
+
accountId: params.accountId ?? undefined,
|
|
142
|
+
});
|
|
143
|
+
const groups = account.config.groups ?? {};
|
|
144
|
+
const entry = findZalouserGroupEntry(
|
|
145
|
+
groups,
|
|
146
|
+
buildZalouserGroupCandidates({
|
|
147
|
+
groupId: params.groupId,
|
|
148
|
+
groupChannel: params.groupChannel,
|
|
149
|
+
includeWildcard: true,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
if (typeof entry?.requireMention === "boolean") {
|
|
153
|
+
return entry.requireMention;
|
|
105
154
|
}
|
|
106
|
-
return
|
|
155
|
+
return true;
|
|
107
156
|
}
|
|
108
157
|
|
|
158
|
+
const zalouserMessageActions: ChannelMessageActionAdapter = {
|
|
159
|
+
listActions: ({ cfg }) => {
|
|
160
|
+
const accounts = listZalouserAccountIds(cfg)
|
|
161
|
+
.map((accountId) => resolveZalouserAccountSync({ cfg, accountId }))
|
|
162
|
+
.filter((account) => account.enabled);
|
|
163
|
+
if (accounts.length === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return ["react"];
|
|
167
|
+
},
|
|
168
|
+
supportsAction: ({ action }) => action === "react",
|
|
169
|
+
handleAction: async ({ action, params, cfg, accountId, toolContext }) => {
|
|
170
|
+
if (action !== "react") {
|
|
171
|
+
throw new Error(`Zalouser action ${action} not supported`);
|
|
172
|
+
}
|
|
173
|
+
const account = resolveZalouserAccountSync({ cfg, accountId });
|
|
174
|
+
const threadId =
|
|
175
|
+
(typeof params.threadId === "string" ? params.threadId.trim() : "") ||
|
|
176
|
+
(typeof params.to === "string" ? params.to.trim() : "") ||
|
|
177
|
+
(typeof params.chatId === "string" ? params.chatId.trim() : "") ||
|
|
178
|
+
(toolContext?.currentChannelId?.trim() ?? "");
|
|
179
|
+
if (!threadId) {
|
|
180
|
+
throw new Error("Zalouser react requires threadId (or to/chatId).");
|
|
181
|
+
}
|
|
182
|
+
const emoji = typeof params.emoji === "string" ? params.emoji.trim() : "";
|
|
183
|
+
if (!emoji) {
|
|
184
|
+
throw new Error("Zalouser react requires emoji.");
|
|
185
|
+
}
|
|
186
|
+
const ids = resolveZalouserReactionMessageIds({
|
|
187
|
+
messageId: typeof params.messageId === "string" ? params.messageId : undefined,
|
|
188
|
+
cliMsgId: typeof params.cliMsgId === "string" ? params.cliMsgId : undefined,
|
|
189
|
+
currentMessageId: toolContext?.currentMessageId,
|
|
190
|
+
});
|
|
191
|
+
if (!ids) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
"Zalouser react requires messageId + cliMsgId (or a current message context id).",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const result = await sendReactionZalouser({
|
|
197
|
+
profile: account.profile,
|
|
198
|
+
threadId,
|
|
199
|
+
isGroup: params.isGroup === true,
|
|
200
|
+
msgId: ids.msgId,
|
|
201
|
+
cliMsgId: ids.cliMsgId,
|
|
202
|
+
emoji,
|
|
203
|
+
remove: params.remove === true,
|
|
204
|
+
});
|
|
205
|
+
if (!result.ok) {
|
|
206
|
+
throw new Error(result.error || "Failed to react on Zalo message");
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text" as const,
|
|
212
|
+
text:
|
|
213
|
+
params.remove === true
|
|
214
|
+
? `Removed reaction ${emoji} from ${ids.msgId}`
|
|
215
|
+
: `Reacted ${emoji} on ${ids.msgId}`,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
details: {
|
|
219
|
+
messageId: ids.msgId,
|
|
220
|
+
cliMsgId: ids.cliMsgId,
|
|
221
|
+
threadId,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
109
227
|
export const zalouserDock: ChannelDock = {
|
|
110
228
|
id: "zalouser",
|
|
111
229
|
capabilities: {
|
|
@@ -123,7 +241,7 @@ export const zalouserDock: ChannelDock = {
|
|
|
123
241
|
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
|
|
124
242
|
},
|
|
125
243
|
groups: {
|
|
126
|
-
resolveRequireMention:
|
|
244
|
+
resolveRequireMention: resolveZalouserRequireMention,
|
|
127
245
|
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
128
246
|
},
|
|
129
247
|
threading: {
|
|
@@ -173,14 +291,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
173
291
|
"messagePrefix",
|
|
174
292
|
],
|
|
175
293
|
}),
|
|
176
|
-
isConfigured: async (account) =>
|
|
177
|
-
// Check if zca auth status is OK for this profile
|
|
178
|
-
const result = await runZca(["auth", "status"], {
|
|
179
|
-
profile: account.profile,
|
|
180
|
-
timeout: 5000,
|
|
181
|
-
});
|
|
182
|
-
return result.ok;
|
|
183
|
-
},
|
|
294
|
+
isConfigured: async (account) => await checkZcaAuthenticated(account.profile),
|
|
184
295
|
describeAccount: (account): ChannelAccountSnapshot => ({
|
|
185
296
|
accountId: account.accountId,
|
|
186
297
|
name: account.name,
|
|
@@ -213,12 +324,13 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
213
324
|
},
|
|
214
325
|
},
|
|
215
326
|
groups: {
|
|
216
|
-
resolveRequireMention:
|
|
327
|
+
resolveRequireMention: resolveZalouserRequireMention,
|
|
217
328
|
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
218
329
|
},
|
|
219
330
|
threading: {
|
|
220
331
|
resolveReplyToMode: () => "off",
|
|
221
332
|
},
|
|
333
|
+
actions: zalouserMessageActions,
|
|
222
334
|
setup: {
|
|
223
335
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
224
336
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
@@ -294,21 +406,9 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
294
406
|
},
|
|
295
407
|
},
|
|
296
408
|
directory: {
|
|
297
|
-
self: async ({ cfg, accountId
|
|
298
|
-
const ok = await checkZcaInstalled();
|
|
299
|
-
if (!ok) {
|
|
300
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
301
|
-
}
|
|
409
|
+
self: async ({ cfg, accountId }) => {
|
|
302
410
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
303
|
-
const
|
|
304
|
-
profile: account.profile,
|
|
305
|
-
timeout: 10000,
|
|
306
|
-
});
|
|
307
|
-
if (!result.ok) {
|
|
308
|
-
runtime.error(result.stderr || "Failed to fetch profile");
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
const parsed = parseJsonOutput<ZcaUserInfo>(result.stdout);
|
|
411
|
+
const parsed = await getZaloUserInfo(account.profile);
|
|
312
412
|
if (!parsed?.userId) {
|
|
313
413
|
return null;
|
|
314
414
|
}
|
|
@@ -320,92 +420,42 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
320
420
|
});
|
|
321
421
|
},
|
|
322
422
|
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
323
|
-
const ok = await checkZcaInstalled();
|
|
324
|
-
if (!ok) {
|
|
325
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
326
|
-
}
|
|
327
423
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
id: String(f.userId),
|
|
338
|
-
name: f.displayName ?? null,
|
|
339
|
-
avatarUrl: f.avatar ?? null,
|
|
340
|
-
raw: f,
|
|
341
|
-
}),
|
|
342
|
-
)
|
|
343
|
-
: [];
|
|
424
|
+
const friends = await listZaloFriendsMatching(account.profile, query);
|
|
425
|
+
const rows = friends.map((friend) =>
|
|
426
|
+
mapUser({
|
|
427
|
+
id: String(friend.userId),
|
|
428
|
+
name: friend.displayName ?? null,
|
|
429
|
+
avatarUrl: friend.avatar ?? null,
|
|
430
|
+
raw: friend,
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
344
433
|
return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
345
434
|
},
|
|
346
435
|
listGroups: async ({ cfg, accountId, query, limit }) => {
|
|
347
|
-
const ok = await checkZcaInstalled();
|
|
348
|
-
if (!ok) {
|
|
349
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
350
|
-
}
|
|
351
436
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
let rows = Array.isArray(parsed)
|
|
361
|
-
? parsed.map((g) =>
|
|
362
|
-
mapGroup({
|
|
363
|
-
id: String(g.groupId),
|
|
364
|
-
name: g.name ?? null,
|
|
365
|
-
raw: g,
|
|
366
|
-
}),
|
|
367
|
-
)
|
|
368
|
-
: [];
|
|
369
|
-
const q = query?.trim().toLowerCase();
|
|
370
|
-
if (q) {
|
|
371
|
-
rows = rows.filter((g) => (g.name ?? "").toLowerCase().includes(q) || g.id.includes(q));
|
|
372
|
-
}
|
|
437
|
+
const groups = await listZaloGroupsMatching(account.profile, query);
|
|
438
|
+
const rows = groups.map((group) =>
|
|
439
|
+
mapGroup({
|
|
440
|
+
id: String(group.groupId),
|
|
441
|
+
name: group.name ?? null,
|
|
442
|
+
raw: group,
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
373
445
|
return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
374
446
|
},
|
|
375
447
|
listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
|
|
376
|
-
const ok = await checkZcaInstalled();
|
|
377
|
-
if (!ok) {
|
|
378
|
-
throw new Error("Missing dependency: `zca` not found in PATH");
|
|
379
|
-
}
|
|
380
448
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
result.stdout,
|
|
449
|
+
const members = await listZaloGroupMembers(account.profile, groupId);
|
|
450
|
+
const rows = members.map((member) =>
|
|
451
|
+
mapUser({
|
|
452
|
+
id: member.userId,
|
|
453
|
+
name: member.displayName,
|
|
454
|
+
avatarUrl: member.avatar ?? null,
|
|
455
|
+
raw: member,
|
|
456
|
+
}),
|
|
390
457
|
);
|
|
391
|
-
|
|
392
|
-
? parsed
|
|
393
|
-
.map((m) => {
|
|
394
|
-
const id = m.userId ?? (m as { id?: string | number }).id;
|
|
395
|
-
if (!id) {
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
return mapUser({
|
|
399
|
-
id: String(id),
|
|
400
|
-
name: (m as { displayName?: string }).displayName ?? null,
|
|
401
|
-
avatarUrl: (m as { avatar?: string }).avatar ?? null,
|
|
402
|
-
raw: m,
|
|
403
|
-
});
|
|
404
|
-
})
|
|
405
|
-
.filter(Boolean)
|
|
406
|
-
: [];
|
|
407
|
-
const sliced = typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
408
|
-
return sliced as ChannelDirectoryEntry[];
|
|
458
|
+
return typeof limit === "number" && limit > 0 ? rows.slice(0, limit) : rows;
|
|
409
459
|
},
|
|
410
460
|
},
|
|
411
461
|
resolver: {
|
|
@@ -426,48 +476,27 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
426
476
|
cfg: cfg,
|
|
427
477
|
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
|
428
478
|
});
|
|
429
|
-
const args =
|
|
430
|
-
kind === "user"
|
|
431
|
-
? trimmed
|
|
432
|
-
? ["friend", "find", trimmed]
|
|
433
|
-
: ["friend", "list", "-j"]
|
|
434
|
-
: ["group", "list", "-j"];
|
|
435
|
-
const result = await runZca(args, { profile: account.profile, timeout: 15000 });
|
|
436
|
-
if (!result.ok) {
|
|
437
|
-
throw new Error(result.stderr || "zca lookup failed");
|
|
438
|
-
}
|
|
439
479
|
if (kind === "user") {
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
? parsed.map((f) => ({
|
|
443
|
-
id: String(f.userId),
|
|
444
|
-
name: f.displayName ?? undefined,
|
|
445
|
-
}))
|
|
446
|
-
: [];
|
|
447
|
-
const best = matches[0];
|
|
480
|
+
const friends = await listZaloFriendsMatching(account.profile, trimmed);
|
|
481
|
+
const best = friends[0];
|
|
448
482
|
results.push({
|
|
449
483
|
input,
|
|
450
|
-
resolved: Boolean(best?.
|
|
451
|
-
id: best?.
|
|
452
|
-
name: best?.
|
|
453
|
-
note:
|
|
484
|
+
resolved: Boolean(best?.userId),
|
|
485
|
+
id: best?.userId,
|
|
486
|
+
name: best?.displayName,
|
|
487
|
+
note: friends.length > 1 ? "multiple matches; chose first" : undefined,
|
|
454
488
|
});
|
|
455
489
|
} else {
|
|
456
|
-
const
|
|
457
|
-
const matches = Array.isArray(parsed)
|
|
458
|
-
? parsed.map((g) => ({
|
|
459
|
-
id: String(g.groupId),
|
|
460
|
-
name: g.name ?? undefined,
|
|
461
|
-
}))
|
|
462
|
-
: [];
|
|
490
|
+
const groups = await listZaloGroupsMatching(account.profile, trimmed);
|
|
463
491
|
const best =
|
|
464
|
-
|
|
492
|
+
groups.find((group) => group.name.toLowerCase() === trimmed.toLowerCase()) ??
|
|
493
|
+
groups[0];
|
|
465
494
|
results.push({
|
|
466
495
|
input,
|
|
467
|
-
resolved: Boolean(best?.
|
|
468
|
-
id: best?.
|
|
496
|
+
resolved: Boolean(best?.groupId),
|
|
497
|
+
id: best?.groupId,
|
|
469
498
|
name: best?.name,
|
|
470
|
-
note:
|
|
499
|
+
note: groups.length > 1 ? "multiple matches; chose first" : undefined,
|
|
471
500
|
});
|
|
472
501
|
}
|
|
473
502
|
} catch (err) {
|
|
@@ -498,19 +527,32 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
498
527
|
cfg: cfg,
|
|
499
528
|
accountId: accountId ?? DEFAULT_ACCOUNT_ID,
|
|
500
529
|
});
|
|
501
|
-
|
|
502
|
-
if (!ok) {
|
|
503
|
-
throw new Error(
|
|
504
|
-
"Missing dependency: `zca` not found in PATH. See docs.openclaw.ai/channels/zalouser",
|
|
505
|
-
);
|
|
506
|
-
}
|
|
530
|
+
|
|
507
531
|
runtime.log(
|
|
508
|
-
`
|
|
532
|
+
`Generating QR login for Zalo Personal (account: ${account.accountId}, profile: ${account.profile})...`,
|
|
509
533
|
);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
534
|
+
|
|
535
|
+
const started = await startZaloQrLogin({
|
|
536
|
+
profile: account.profile,
|
|
537
|
+
timeoutMs: 35_000,
|
|
538
|
+
});
|
|
539
|
+
if (!started.qrDataUrl) {
|
|
540
|
+
throw new Error(started.message || "Failed to start QR login");
|
|
513
541
|
}
|
|
542
|
+
|
|
543
|
+
const qrPath = await writeQrDataUrlToTempFile(started.qrDataUrl, account.profile);
|
|
544
|
+
if (qrPath) {
|
|
545
|
+
runtime.log(`Scan QR image: ${qrPath}`);
|
|
546
|
+
} else {
|
|
547
|
+
runtime.log("QR generated but could not be written to a temp file.");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 180_000 });
|
|
551
|
+
if (!waited.connected) {
|
|
552
|
+
throw new Error(waited.message || "Zalouser login failed");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
runtime.log(waited.message);
|
|
514
556
|
},
|
|
515
557
|
},
|
|
516
558
|
outbound: {
|
|
@@ -518,6 +560,40 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
518
560
|
chunker: chunkTextForOutbound,
|
|
519
561
|
chunkerMode: "text",
|
|
520
562
|
textChunkLimit: 2000,
|
|
563
|
+
sendPayload: async (ctx) => {
|
|
564
|
+
const text = ctx.payload.text ?? "";
|
|
565
|
+
const urls = ctx.payload.mediaUrls?.length
|
|
566
|
+
? ctx.payload.mediaUrls
|
|
567
|
+
: ctx.payload.mediaUrl
|
|
568
|
+
? [ctx.payload.mediaUrl]
|
|
569
|
+
: [];
|
|
570
|
+
if (!text && urls.length === 0) {
|
|
571
|
+
return { channel: "zalouser", messageId: "" };
|
|
572
|
+
}
|
|
573
|
+
if (urls.length > 0) {
|
|
574
|
+
let lastResult = await zalouserPlugin.outbound!.sendMedia!({
|
|
575
|
+
...ctx,
|
|
576
|
+
text,
|
|
577
|
+
mediaUrl: urls[0],
|
|
578
|
+
});
|
|
579
|
+
for (let i = 1; i < urls.length; i++) {
|
|
580
|
+
lastResult = await zalouserPlugin.outbound!.sendMedia!({
|
|
581
|
+
...ctx,
|
|
582
|
+
text: "",
|
|
583
|
+
mediaUrl: urls[i],
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return lastResult;
|
|
587
|
+
}
|
|
588
|
+
const outbound = zalouserPlugin.outbound!;
|
|
589
|
+
const limit = outbound.textChunkLimit;
|
|
590
|
+
const chunks = limit && outbound.chunker ? outbound.chunker(text, limit) : [text];
|
|
591
|
+
let lastResult: Awaited<ReturnType<NonNullable<typeof outbound.sendText>>>;
|
|
592
|
+
for (const chunk of chunks) {
|
|
593
|
+
lastResult = await outbound.sendText!({ ...ctx, text: chunk });
|
|
594
|
+
}
|
|
595
|
+
return lastResult!;
|
|
596
|
+
},
|
|
521
597
|
sendText: async ({ to, text, accountId, cfg }) => {
|
|
522
598
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
523
599
|
const result = await sendMessageZalouser(to, text, { profile: account.profile });
|
|
@@ -528,11 +604,12 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
528
604
|
error: result.error ? new Error(result.error) : undefined,
|
|
529
605
|
};
|
|
530
606
|
},
|
|
531
|
-
sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
|
|
607
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots }) => {
|
|
532
608
|
const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
533
609
|
const result = await sendMessageZalouser(to, text, {
|
|
534
610
|
profile: account.profile,
|
|
535
611
|
mediaUrl,
|
|
612
|
+
mediaLocalRoots,
|
|
536
613
|
});
|
|
537
614
|
return {
|
|
538
615
|
channel: "zalouser",
|
|
@@ -562,9 +639,8 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
562
639
|
}),
|
|
563
640
|
probeAccount: async ({ account, timeoutMs }) => probeZalouser(account.profile, timeoutMs),
|
|
564
641
|
buildAccountSnapshot: async ({ account, runtime }) => {
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const configError = zcaInstalled ? "not authenticated" : "zca CLI not found in PATH";
|
|
642
|
+
const configured = await checkZcaAuthenticated(account.profile);
|
|
643
|
+
const configError = "not authenticated";
|
|
568
644
|
return {
|
|
569
645
|
accountId: account.accountId,
|
|
570
646
|
name: account.name,
|
|
@@ -608,44 +684,21 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
608
684
|
},
|
|
609
685
|
loginWithQrStart: async (params) => {
|
|
610
686
|
const profile = resolveZalouserQrProfile(params.accountId);
|
|
611
|
-
|
|
612
|
-
const result = await runZca(["auth", "login", "--qr-base64"], {
|
|
687
|
+
return await startZaloQrLogin({
|
|
613
688
|
profile,
|
|
614
|
-
|
|
689
|
+
force: params.force,
|
|
690
|
+
timeoutMs: params.timeoutMs,
|
|
615
691
|
});
|
|
616
|
-
if (!result.ok) {
|
|
617
|
-
return { message: result.stderr || "Failed to start QR login" };
|
|
618
|
-
}
|
|
619
|
-
// The stdout should contain the base64 QR data URL
|
|
620
|
-
const qrMatch = result.stdout.match(/data:image\/png;base64,[A-Za-z0-9+/=]+/);
|
|
621
|
-
if (qrMatch) {
|
|
622
|
-
return { qrDataUrl: qrMatch[0], message: "Scan QR code with Zalo app" };
|
|
623
|
-
}
|
|
624
|
-
return { message: result.stdout || "QR login started" };
|
|
625
692
|
},
|
|
626
693
|
loginWithQrWait: async (params) => {
|
|
627
694
|
const profile = resolveZalouserQrProfile(params.accountId);
|
|
628
|
-
|
|
629
|
-
const statusResult = await runZca(["auth", "status"], {
|
|
695
|
+
return await waitForZaloQrLogin({
|
|
630
696
|
profile,
|
|
631
|
-
|
|
697
|
+
timeoutMs: params.timeoutMs,
|
|
632
698
|
});
|
|
633
|
-
return {
|
|
634
|
-
connected: statusResult.ok,
|
|
635
|
-
message: statusResult.ok ? "Login successful" : statusResult.stderr || "Login pending",
|
|
636
|
-
};
|
|
637
|
-
},
|
|
638
|
-
logoutAccount: async (ctx) => {
|
|
639
|
-
const result = await runZca(["auth", "logout"], {
|
|
640
|
-
profile: ctx.account.profile,
|
|
641
|
-
timeout: 10000,
|
|
642
|
-
});
|
|
643
|
-
return {
|
|
644
|
-
cleared: result.ok,
|
|
645
|
-
loggedOut: result.ok,
|
|
646
|
-
message: result.ok ? "Logged out" : result.stderr,
|
|
647
|
-
};
|
|
648
699
|
},
|
|
700
|
+
logoutAccount: async (ctx) =>
|
|
701
|
+
await logoutZaloProfile(ctx.account.profile || resolveZalouserQrProfile(ctx.accountId)),
|
|
649
702
|
},
|
|
650
703
|
};
|
|
651
704
|
|
package/src/config-schema.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildZalouserGroupCandidates,
|
|
4
|
+
findZalouserGroupEntry,
|
|
5
|
+
isZalouserGroupEntryAllowed,
|
|
6
|
+
normalizeZalouserGroupSlug,
|
|
7
|
+
} from "./group-policy.js";
|
|
8
|
+
|
|
9
|
+
describe("zalouser group policy helpers", () => {
|
|
10
|
+
it("normalizes group slug names", () => {
|
|
11
|
+
expect(normalizeZalouserGroupSlug(" Team Alpha ")).toBe("team-alpha");
|
|
12
|
+
expect(normalizeZalouserGroupSlug("#Roadmap Updates")).toBe("roadmap-updates");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("builds ordered candidates with optional aliases", () => {
|
|
16
|
+
expect(
|
|
17
|
+
buildZalouserGroupCandidates({
|
|
18
|
+
groupId: "123",
|
|
19
|
+
groupChannel: "chan-1",
|
|
20
|
+
groupName: "Team Alpha",
|
|
21
|
+
includeGroupIdAlias: true,
|
|
22
|
+
}),
|
|
23
|
+
).toEqual(["123", "group:123", "chan-1", "Team Alpha", "team-alpha", "*"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("finds the first matching group entry", () => {
|
|
27
|
+
const groups = {
|
|
28
|
+
"group:123": { allow: true },
|
|
29
|
+
"team-alpha": { requireMention: false },
|
|
30
|
+
"*": { requireMention: true },
|
|
31
|
+
};
|
|
32
|
+
const entry = findZalouserGroupEntry(
|
|
33
|
+
groups,
|
|
34
|
+
buildZalouserGroupCandidates({
|
|
35
|
+
groupId: "123",
|
|
36
|
+
groupName: "Team Alpha",
|
|
37
|
+
includeGroupIdAlias: true,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
expect(entry).toEqual({ allow: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("evaluates allow/enable flags", () => {
|
|
44
|
+
expect(isZalouserGroupEntryAllowed({ allow: true, enabled: true })).toBe(true);
|
|
45
|
+
expect(isZalouserGroupEntryAllowed({ allow: false })).toBe(false);
|
|
46
|
+
expect(isZalouserGroupEntryAllowed({ enabled: false })).toBe(false);
|
|
47
|
+
expect(isZalouserGroupEntryAllowed(undefined)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|