@openclaw/zalouser 2026.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/CHANGELOG.md +38 -0
- package/README.md +221 -0
- package/index.ts +32 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +33 -0
- package/src/accounts.ts +117 -0
- package/src/channel.test.ts +17 -0
- package/src/channel.ts +641 -0
- package/src/config-schema.ts +27 -0
- package/src/monitor.ts +574 -0
- package/src/onboarding.ts +488 -0
- package/src/probe.ts +28 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +150 -0
- package/src/status-issues.test.ts +58 -0
- package/src/status-issues.ts +81 -0
- package/src/tool.ts +156 -0
- package/src/types.ts +102 -0
- package/src/zca.ts +208 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
ChannelOnboardingDmPolicy,
|
|
4
|
+
OpenClawConfig,
|
|
5
|
+
WizardPrompter,
|
|
6
|
+
} from "openclaw/plugin-sdk";
|
|
7
|
+
import {
|
|
8
|
+
addWildcardAllowFrom,
|
|
9
|
+
DEFAULT_ACCOUNT_ID,
|
|
10
|
+
normalizeAccountId,
|
|
11
|
+
promptAccountId,
|
|
12
|
+
promptChannelAccessConfig,
|
|
13
|
+
} from "openclaw/plugin-sdk";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
listZalouserAccountIds,
|
|
17
|
+
resolveDefaultZalouserAccountId,
|
|
18
|
+
resolveZalouserAccountSync,
|
|
19
|
+
checkZcaAuthenticated,
|
|
20
|
+
} from "./accounts.js";
|
|
21
|
+
import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
|
|
22
|
+
import type { ZcaFriend, ZcaGroup } from "./types.js";
|
|
23
|
+
|
|
24
|
+
const channel = "zalouser" as const;
|
|
25
|
+
|
|
26
|
+
function setZalouserDmPolicy(
|
|
27
|
+
cfg: OpenClawConfig,
|
|
28
|
+
dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
|
|
29
|
+
): OpenClawConfig {
|
|
30
|
+
const allowFrom =
|
|
31
|
+
dmPolicy === "open"
|
|
32
|
+
? addWildcardAllowFrom(cfg.channels?.zalouser?.allowFrom)
|
|
33
|
+
: undefined;
|
|
34
|
+
return {
|
|
35
|
+
...cfg,
|
|
36
|
+
channels: {
|
|
37
|
+
...cfg.channels,
|
|
38
|
+
zalouser: {
|
|
39
|
+
...cfg.channels?.zalouser,
|
|
40
|
+
dmPolicy,
|
|
41
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} as OpenClawConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
|
|
48
|
+
await prompter.note(
|
|
49
|
+
[
|
|
50
|
+
"Zalo Personal Account login via QR code.",
|
|
51
|
+
"",
|
|
52
|
+
"Prerequisites:",
|
|
53
|
+
"1) Install zca-cli",
|
|
54
|
+
"2) You'll scan a QR code with your Zalo app",
|
|
55
|
+
"",
|
|
56
|
+
"Docs: https://docs.openclaw.ai/channels/zalouser",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
"Zalo Personal Setup",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function promptZalouserAllowFrom(params: {
|
|
63
|
+
cfg: OpenClawConfig;
|
|
64
|
+
prompter: WizardPrompter;
|
|
65
|
+
accountId: string;
|
|
66
|
+
}): Promise<OpenClawConfig> {
|
|
67
|
+
const { cfg, prompter, accountId } = params;
|
|
68
|
+
const resolved = resolveZalouserAccountSync({ cfg, accountId });
|
|
69
|
+
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
|
70
|
+
const parseInput = (raw: string) =>
|
|
71
|
+
raw
|
|
72
|
+
.split(/[\n,;]+/g)
|
|
73
|
+
.map((entry) => entry.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
|
|
76
|
+
const resolveUserId = async (input: string): Promise<string | null> => {
|
|
77
|
+
const trimmed = input.trim();
|
|
78
|
+
if (!trimmed) return null;
|
|
79
|
+
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
80
|
+
const ok = await checkZcaInstalled();
|
|
81
|
+
if (!ok) return null;
|
|
82
|
+
const result = await runZca(["friend", "find", trimmed], {
|
|
83
|
+
profile: resolved.profile,
|
|
84
|
+
timeout: 15000,
|
|
85
|
+
});
|
|
86
|
+
if (!result.ok) return null;
|
|
87
|
+
const parsed = parseJsonOutput<ZcaFriend[]>(result.stdout);
|
|
88
|
+
const rows = Array.isArray(parsed) ? parsed : [];
|
|
89
|
+
const match = rows[0];
|
|
90
|
+
if (!match?.userId) return null;
|
|
91
|
+
if (rows.length > 1) {
|
|
92
|
+
await prompter.note(
|
|
93
|
+
`Multiple matches for "${trimmed}", using ${match.displayName ?? match.userId}.`,
|
|
94
|
+
"Zalo Personal allowlist",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return String(match.userId);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
while (true) {
|
|
101
|
+
const entry = await prompter.text({
|
|
102
|
+
message: "Zalouser allowFrom (username or user id)",
|
|
103
|
+
placeholder: "Alice, 123456789",
|
|
104
|
+
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
105
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
106
|
+
});
|
|
107
|
+
const parts = parseInput(String(entry));
|
|
108
|
+
const results = await Promise.all(parts.map((part) => resolveUserId(part)));
|
|
109
|
+
const unresolved = parts.filter((_, idx) => !results[idx]);
|
|
110
|
+
if (unresolved.length > 0) {
|
|
111
|
+
await prompter.note(
|
|
112
|
+
`Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or ensure zca is available.`,
|
|
113
|
+
"Zalo Personal allowlist",
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const merged = [
|
|
118
|
+
...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
|
|
119
|
+
...(results.filter(Boolean) as string[]),
|
|
120
|
+
];
|
|
121
|
+
const unique = [...new Set(merged)];
|
|
122
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
123
|
+
return {
|
|
124
|
+
...cfg,
|
|
125
|
+
channels: {
|
|
126
|
+
...cfg.channels,
|
|
127
|
+
zalouser: {
|
|
128
|
+
...cfg.channels?.zalouser,
|
|
129
|
+
enabled: true,
|
|
130
|
+
dmPolicy: "allowlist",
|
|
131
|
+
allowFrom: unique,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
} as OpenClawConfig;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
...cfg,
|
|
139
|
+
channels: {
|
|
140
|
+
...cfg.channels,
|
|
141
|
+
zalouser: {
|
|
142
|
+
...cfg.channels?.zalouser,
|
|
143
|
+
enabled: true,
|
|
144
|
+
accounts: {
|
|
145
|
+
...(cfg.channels?.zalouser?.accounts ?? {}),
|
|
146
|
+
[accountId]: {
|
|
147
|
+
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
|
148
|
+
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
|
149
|
+
dmPolicy: "allowlist",
|
|
150
|
+
allowFrom: unique,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
} as OpenClawConfig;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setZalouserGroupPolicy(
|
|
160
|
+
cfg: OpenClawConfig,
|
|
161
|
+
accountId: string,
|
|
162
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
163
|
+
): OpenClawConfig {
|
|
164
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
165
|
+
return {
|
|
166
|
+
...cfg,
|
|
167
|
+
channels: {
|
|
168
|
+
...cfg.channels,
|
|
169
|
+
zalouser: {
|
|
170
|
+
...cfg.channels?.zalouser,
|
|
171
|
+
enabled: true,
|
|
172
|
+
groupPolicy,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
} as OpenClawConfig;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
...cfg,
|
|
179
|
+
channels: {
|
|
180
|
+
...cfg.channels,
|
|
181
|
+
zalouser: {
|
|
182
|
+
...cfg.channels?.zalouser,
|
|
183
|
+
enabled: true,
|
|
184
|
+
accounts: {
|
|
185
|
+
...(cfg.channels?.zalouser?.accounts ?? {}),
|
|
186
|
+
[accountId]: {
|
|
187
|
+
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
|
188
|
+
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
|
189
|
+
groupPolicy,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
} as OpenClawConfig;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function setZalouserGroupAllowlist(
|
|
198
|
+
cfg: OpenClawConfig,
|
|
199
|
+
accountId: string,
|
|
200
|
+
groupKeys: string[],
|
|
201
|
+
): OpenClawConfig {
|
|
202
|
+
const groups = Object.fromEntries(groupKeys.map((key) => [key, { allow: true }]));
|
|
203
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
204
|
+
return {
|
|
205
|
+
...cfg,
|
|
206
|
+
channels: {
|
|
207
|
+
...cfg.channels,
|
|
208
|
+
zalouser: {
|
|
209
|
+
...cfg.channels?.zalouser,
|
|
210
|
+
enabled: true,
|
|
211
|
+
groups,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
} as OpenClawConfig;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
...cfg,
|
|
218
|
+
channels: {
|
|
219
|
+
...cfg.channels,
|
|
220
|
+
zalouser: {
|
|
221
|
+
...cfg.channels?.zalouser,
|
|
222
|
+
enabled: true,
|
|
223
|
+
accounts: {
|
|
224
|
+
...(cfg.channels?.zalouser?.accounts ?? {}),
|
|
225
|
+
[accountId]: {
|
|
226
|
+
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
|
227
|
+
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
|
228
|
+
groups,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
} as OpenClawConfig;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function resolveZalouserGroups(params: {
|
|
237
|
+
cfg: OpenClawConfig;
|
|
238
|
+
accountId: string;
|
|
239
|
+
entries: string[];
|
|
240
|
+
}): Promise<Array<{ input: string; resolved: boolean; id?: string }>> {
|
|
241
|
+
const account = resolveZalouserAccountSync({ cfg: params.cfg, accountId: params.accountId });
|
|
242
|
+
const result = await runZca(["group", "list", "-j"], { profile: account.profile, timeout: 15000 });
|
|
243
|
+
if (!result.ok) throw new Error(result.stderr || "Failed to list groups");
|
|
244
|
+
const groups = (parseJsonOutput<ZcaGroup[]>(result.stdout) ?? []).filter(
|
|
245
|
+
(group) => Boolean(group.groupId),
|
|
246
|
+
);
|
|
247
|
+
const byName = new Map<string, ZcaGroup[]>();
|
|
248
|
+
for (const group of groups) {
|
|
249
|
+
const name = group.name?.trim().toLowerCase();
|
|
250
|
+
if (!name) continue;
|
|
251
|
+
const list = byName.get(name) ?? [];
|
|
252
|
+
list.push(group);
|
|
253
|
+
byName.set(name, list);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return params.entries.map((input) => {
|
|
257
|
+
const trimmed = input.trim();
|
|
258
|
+
if (!trimmed) return { input, resolved: false };
|
|
259
|
+
if (/^\d+$/.test(trimmed)) return { input, resolved: true, id: trimmed };
|
|
260
|
+
const matches = byName.get(trimmed.toLowerCase()) ?? [];
|
|
261
|
+
const match = matches[0];
|
|
262
|
+
return match?.groupId
|
|
263
|
+
? { input, resolved: true, id: String(match.groupId) }
|
|
264
|
+
: { input, resolved: false };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
269
|
+
label: "Zalo Personal",
|
|
270
|
+
channel,
|
|
271
|
+
policyKey: "channels.zalouser.dmPolicy",
|
|
272
|
+
allowFromKey: "channels.zalouser.allowFrom",
|
|
273
|
+
getCurrent: (cfg) => ((cfg as OpenClawConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
|
|
274
|
+
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as OpenClawConfig, policy),
|
|
275
|
+
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
|
276
|
+
const id =
|
|
277
|
+
accountId && normalizeAccountId(accountId)
|
|
278
|
+
? normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID
|
|
279
|
+
: resolveDefaultZalouserAccountId(cfg as OpenClawConfig);
|
|
280
|
+
return promptZalouserAllowFrom({
|
|
281
|
+
cfg: cfg as OpenClawConfig,
|
|
282
|
+
prompter,
|
|
283
|
+
accountId: id,
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
289
|
+
channel,
|
|
290
|
+
dmPolicy,
|
|
291
|
+
getStatus: async ({ cfg }) => {
|
|
292
|
+
const ids = listZalouserAccountIds(cfg as OpenClawConfig);
|
|
293
|
+
let configured = false;
|
|
294
|
+
for (const accountId of ids) {
|
|
295
|
+
const account = resolveZalouserAccountSync({ cfg: cfg as OpenClawConfig, accountId });
|
|
296
|
+
const isAuth = await checkZcaAuthenticated(account.profile);
|
|
297
|
+
if (isAuth) {
|
|
298
|
+
configured = true;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
channel,
|
|
304
|
+
configured,
|
|
305
|
+
statusLines: [`Zalo Personal: ${configured ? "logged in" : "needs QR login"}`],
|
|
306
|
+
selectionHint: configured ? "recommended · logged in" : "recommended · QR login",
|
|
307
|
+
quickstartScore: configured ? 1 : 15,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds, forceAllowFrom }) => {
|
|
311
|
+
// Check zca is installed
|
|
312
|
+
const zcaInstalled = await checkZcaInstalled();
|
|
313
|
+
if (!zcaInstalled) {
|
|
314
|
+
await prompter.note(
|
|
315
|
+
[
|
|
316
|
+
"The `zca` binary was not found in PATH.",
|
|
317
|
+
"",
|
|
318
|
+
"Install zca-cli, then re-run onboarding:",
|
|
319
|
+
"Docs: https://docs.openclaw.ai/channels/zalouser",
|
|
320
|
+
].join("\n"),
|
|
321
|
+
"Missing Dependency",
|
|
322
|
+
);
|
|
323
|
+
return { cfg, accountId: DEFAULT_ACCOUNT_ID };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const zalouserOverride = accountOverrides.zalouser?.trim();
|
|
327
|
+
const defaultAccountId = resolveDefaultZalouserAccountId(cfg as OpenClawConfig);
|
|
328
|
+
let accountId = zalouserOverride
|
|
329
|
+
? normalizeAccountId(zalouserOverride)
|
|
330
|
+
: defaultAccountId;
|
|
331
|
+
|
|
332
|
+
if (shouldPromptAccountIds && !zalouserOverride) {
|
|
333
|
+
accountId = await promptAccountId({
|
|
334
|
+
cfg: cfg as OpenClawConfig,
|
|
335
|
+
prompter,
|
|
336
|
+
label: "Zalo Personal",
|
|
337
|
+
currentId: accountId,
|
|
338
|
+
listAccountIds: listZalouserAccountIds,
|
|
339
|
+
defaultAccountId,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let next = cfg as OpenClawConfig;
|
|
344
|
+
const account = resolveZalouserAccountSync({ cfg: next, accountId });
|
|
345
|
+
const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
|
|
346
|
+
|
|
347
|
+
if (!alreadyAuthenticated) {
|
|
348
|
+
await noteZalouserHelp(prompter);
|
|
349
|
+
|
|
350
|
+
const wantsLogin = await prompter.confirm({
|
|
351
|
+
message: "Login via QR code now?",
|
|
352
|
+
initialValue: true,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (wantsLogin) {
|
|
356
|
+
await prompter.note(
|
|
357
|
+
"A QR code will appear in your terminal.\nScan it with your Zalo app to login.",
|
|
358
|
+
"QR Login",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Run interactive login
|
|
362
|
+
const result = await runZcaInteractive(["auth", "login"], {
|
|
363
|
+
profile: account.profile,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!result.ok) {
|
|
367
|
+
await prompter.note(
|
|
368
|
+
`Login failed: ${result.stderr || "Unknown error"}`,
|
|
369
|
+
"Error",
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
const isNowAuth = await checkZcaAuthenticated(account.profile);
|
|
373
|
+
if (isNowAuth) {
|
|
374
|
+
await prompter.note("Login successful!", "Success");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
const keepSession = await prompter.confirm({
|
|
380
|
+
message: "Zalo Personal already logged in. Keep session?",
|
|
381
|
+
initialValue: true,
|
|
382
|
+
});
|
|
383
|
+
if (!keepSession) {
|
|
384
|
+
await runZcaInteractive(["auth", "logout"], { profile: account.profile });
|
|
385
|
+
await runZcaInteractive(["auth", "login"], { profile: account.profile });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Enable the channel
|
|
390
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
391
|
+
next = {
|
|
392
|
+
...next,
|
|
393
|
+
channels: {
|
|
394
|
+
...next.channels,
|
|
395
|
+
zalouser: {
|
|
396
|
+
...next.channels?.zalouser,
|
|
397
|
+
enabled: true,
|
|
398
|
+
profile: account.profile !== "default" ? account.profile : undefined,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
} as OpenClawConfig;
|
|
402
|
+
} else {
|
|
403
|
+
next = {
|
|
404
|
+
...next,
|
|
405
|
+
channels: {
|
|
406
|
+
...next.channels,
|
|
407
|
+
zalouser: {
|
|
408
|
+
...next.channels?.zalouser,
|
|
409
|
+
enabled: true,
|
|
410
|
+
accounts: {
|
|
411
|
+
...(next.channels?.zalouser?.accounts ?? {}),
|
|
412
|
+
[accountId]: {
|
|
413
|
+
...(next.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
|
414
|
+
enabled: true,
|
|
415
|
+
profile: account.profile,
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
} as OpenClawConfig;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (forceAllowFrom) {
|
|
424
|
+
next = await promptZalouserAllowFrom({
|
|
425
|
+
cfg: next,
|
|
426
|
+
prompter,
|
|
427
|
+
accountId,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const accessConfig = await promptChannelAccessConfig({
|
|
432
|
+
prompter,
|
|
433
|
+
label: "Zalo groups",
|
|
434
|
+
currentPolicy: account.config.groupPolicy ?? "open",
|
|
435
|
+
currentEntries: Object.keys(account.config.groups ?? {}),
|
|
436
|
+
placeholder: "Family, Work, 123456789",
|
|
437
|
+
updatePrompt: Boolean(account.config.groups),
|
|
438
|
+
});
|
|
439
|
+
if (accessConfig) {
|
|
440
|
+
if (accessConfig.policy !== "allowlist") {
|
|
441
|
+
next = setZalouserGroupPolicy(next, accountId, accessConfig.policy);
|
|
442
|
+
} else {
|
|
443
|
+
let keys = accessConfig.entries;
|
|
444
|
+
if (accessConfig.entries.length > 0) {
|
|
445
|
+
try {
|
|
446
|
+
const resolved = await resolveZalouserGroups({
|
|
447
|
+
cfg: next,
|
|
448
|
+
accountId,
|
|
449
|
+
entries: accessConfig.entries,
|
|
450
|
+
});
|
|
451
|
+
const resolvedIds = resolved
|
|
452
|
+
.filter((entry) => entry.resolved && entry.id)
|
|
453
|
+
.map((entry) => entry.id as string);
|
|
454
|
+
const unresolved = resolved
|
|
455
|
+
.filter((entry) => !entry.resolved)
|
|
456
|
+
.map((entry) => entry.input);
|
|
457
|
+
keys = [
|
|
458
|
+
...resolvedIds,
|
|
459
|
+
...unresolved.map((entry) => entry.trim()).filter(Boolean),
|
|
460
|
+
];
|
|
461
|
+
if (resolvedIds.length > 0 || unresolved.length > 0) {
|
|
462
|
+
await prompter.note(
|
|
463
|
+
[
|
|
464
|
+
resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
|
|
465
|
+
unresolved.length > 0
|
|
466
|
+
? `Unresolved (kept as typed): ${unresolved.join(", ")}`
|
|
467
|
+
: undefined,
|
|
468
|
+
]
|
|
469
|
+
.filter(Boolean)
|
|
470
|
+
.join("\n"),
|
|
471
|
+
"Zalo groups",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
} catch (err) {
|
|
475
|
+
await prompter.note(
|
|
476
|
+
`Group lookup failed; keeping entries as typed. ${String(err)}`,
|
|
477
|
+
"Zalo groups",
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
next = setZalouserGroupPolicy(next, accountId, "allowlist");
|
|
482
|
+
next = setZalouserGroupAllowlist(next, accountId, keys);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { cfg: next, accountId };
|
|
487
|
+
},
|
|
488
|
+
};
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { runZca, parseJsonOutput } from "./zca.js";
|
|
2
|
+
import type { ZcaUserInfo } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface ZalouserProbeResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
user?: ZcaUserInfo;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function probeZalouser(
|
|
11
|
+
profile: string,
|
|
12
|
+
timeoutMs?: number,
|
|
13
|
+
): Promise<ZalouserProbeResult> {
|
|
14
|
+
const result = await runZca(["me", "info", "-j"], {
|
|
15
|
+
profile,
|
|
16
|
+
timeout: timeoutMs,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
return { ok: false, error: result.stderr || "Failed to probe" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const user = parseJsonOutput<ZcaUserInfo>(result.stdout);
|
|
24
|
+
if (!user) {
|
|
25
|
+
return { ok: false, error: "Failed to parse user info" };
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, user };
|
|
28
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setZalouserRuntime(next: PluginRuntime): void {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getZalouserRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("Zalouser runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
package/src/send.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { runZca } from "./zca.js";
|
|
2
|
+
|
|
3
|
+
export type ZalouserSendOptions = {
|
|
4
|
+
profile?: string;
|
|
5
|
+
mediaUrl?: string;
|
|
6
|
+
caption?: string;
|
|
7
|
+
isGroup?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ZalouserSendResult = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
messageId?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function sendMessageZalouser(
|
|
17
|
+
threadId: string,
|
|
18
|
+
text: string,
|
|
19
|
+
options: ZalouserSendOptions = {},
|
|
20
|
+
): Promise<ZalouserSendResult> {
|
|
21
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
22
|
+
|
|
23
|
+
if (!threadId?.trim()) {
|
|
24
|
+
return { ok: false, error: "No threadId provided" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle media sending
|
|
28
|
+
if (options.mediaUrl) {
|
|
29
|
+
return sendMediaZalouser(threadId, options.mediaUrl, {
|
|
30
|
+
...options,
|
|
31
|
+
caption: text || options.caption,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Send text message
|
|
36
|
+
const args = ["msg", "send", threadId.trim(), text.slice(0, 2000)];
|
|
37
|
+
if (options.isGroup) args.push("-g");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await runZca(args, { profile });
|
|
41
|
+
|
|
42
|
+
if (result.ok) {
|
|
43
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { ok: false, error: result.stderr || "Failed to send message" };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function sendMediaZalouser(
|
|
53
|
+
threadId: string,
|
|
54
|
+
mediaUrl: string,
|
|
55
|
+
options: ZalouserSendOptions = {},
|
|
56
|
+
): Promise<ZalouserSendResult> {
|
|
57
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
58
|
+
|
|
59
|
+
if (!threadId?.trim()) {
|
|
60
|
+
return { ok: false, error: "No threadId provided" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!mediaUrl?.trim()) {
|
|
64
|
+
return { ok: false, error: "No media URL provided" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine media type from URL
|
|
68
|
+
const lowerUrl = mediaUrl.toLowerCase();
|
|
69
|
+
let command: string;
|
|
70
|
+
if (lowerUrl.match(/\.(mp4|mov|avi|webm)$/)) {
|
|
71
|
+
command = "video";
|
|
72
|
+
} else if (lowerUrl.match(/\.(mp3|wav|ogg|m4a)$/)) {
|
|
73
|
+
command = "voice";
|
|
74
|
+
} else {
|
|
75
|
+
command = "image";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const args = ["msg", command, threadId.trim(), "-u", mediaUrl.trim()];
|
|
79
|
+
if (options.caption) {
|
|
80
|
+
args.push("-m", options.caption.slice(0, 2000));
|
|
81
|
+
}
|
|
82
|
+
if (options.isGroup) args.push("-g");
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await runZca(args, { profile });
|
|
86
|
+
|
|
87
|
+
if (result.ok) {
|
|
88
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ok: false, error: result.stderr || `Failed to send ${command}` };
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function sendImageZalouser(
|
|
98
|
+
threadId: string,
|
|
99
|
+
imageUrl: string,
|
|
100
|
+
options: ZalouserSendOptions = {},
|
|
101
|
+
): Promise<ZalouserSendResult> {
|
|
102
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
103
|
+
const args = ["msg", "image", threadId.trim(), "-u", imageUrl.trim()];
|
|
104
|
+
if (options.caption) {
|
|
105
|
+
args.push("-m", options.caption.slice(0, 2000));
|
|
106
|
+
}
|
|
107
|
+
if (options.isGroup) args.push("-g");
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await runZca(args, { profile });
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
113
|
+
}
|
|
114
|
+
return { ok: false, error: result.stderr || "Failed to send image" };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function sendLinkZalouser(
|
|
121
|
+
threadId: string,
|
|
122
|
+
url: string,
|
|
123
|
+
options: ZalouserSendOptions = {},
|
|
124
|
+
): Promise<ZalouserSendResult> {
|
|
125
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
126
|
+
const args = ["msg", "link", threadId.trim(), url.trim()];
|
|
127
|
+
if (options.isGroup) args.push("-g");
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await runZca(args, { profile });
|
|
131
|
+
if (result.ok) {
|
|
132
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, error: result.stderr || "Failed to send link" };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extractMessageId(stdout: string): string | undefined {
|
|
141
|
+
// Try to extract message ID from output
|
|
142
|
+
const match = stdout.match(/message[_\s]?id[:\s]+(\S+)/i);
|
|
143
|
+
if (match) return match[1];
|
|
144
|
+
// Return first word if it looks like an ID
|
|
145
|
+
const firstWord = stdout.trim().split(/\s+/)[0];
|
|
146
|
+
if (firstWord && /^[a-zA-Z0-9_-]+$/.test(firstWord)) {
|
|
147
|
+
return firstWord;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|