@openclaw/zalouser 2026.3.13 → 2026.5.1-beta.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/README.md +4 -3
- package/api.ts +9 -0
- package/channel-plugin-api.ts +3 -0
- package/contract-api.ts +2 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +29 -24
- package/openclaw.plugin.json +288 -1
- package/package.json +38 -11
- package/runtime-api.ts +67 -0
- package/secret-contract-api.ts +4 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +2 -0
- package/src/accounts.runtime.ts +1 -0
- package/src/accounts.test-mocks.ts +7 -3
- package/src/accounts.test.ts +53 -1
- package/src/accounts.ts +38 -24
- package/src/channel-api.ts +20 -0
- package/src/channel.adapters.ts +390 -0
- package/src/channel.directory.test.ts +47 -40
- package/src/channel.runtime.ts +12 -0
- package/src/channel.sendpayload.test.ts +41 -23
- package/src/channel.setup.test.ts +33 -0
- package/src/channel.setup.ts +12 -0
- package/src/channel.test.ts +231 -20
- package/src/channel.ts +176 -685
- package/src/config-schema.ts +5 -5
- package/src/directory.ts +54 -0
- package/src/doctor-contract.ts +156 -0
- package/src/doctor.test.ts +77 -0
- package/src/doctor.ts +37 -0
- package/src/group-policy.test.ts +4 -4
- package/src/group-policy.ts +4 -2
- package/src/monitor.account-scope.test.ts +2 -1
- package/src/monitor.group-gating.test.ts +162 -8
- package/src/monitor.ts +233 -173
- package/src/probe.ts +3 -2
- package/src/qr-temp-file.ts +1 -1
- package/src/reaction.ts +5 -2
- package/src/runtime.ts +6 -3
- package/src/security-audit.test.ts +80 -0
- package/src/security-audit.ts +71 -0
- package/src/send.test.ts +2 -2
- package/src/send.ts +3 -3
- package/src/session-route.ts +121 -0
- package/src/setup-core.ts +33 -0
- package/src/setup-surface.test.ts +363 -0
- package/src/setup-surface.ts +470 -0
- package/src/setup-test-helpers.ts +42 -0
- package/src/shared.ts +92 -0
- package/src/status-issues.test.ts +1 -13
- package/src/status-issues.ts +8 -2
- package/src/test-helpers.ts +1 -1
- package/src/text-styles.test.ts +1 -1
- package/src/text-styles.ts +5 -2
- package/src/tool.test.ts +66 -3
- package/src/tool.ts +76 -14
- package/src/types.ts +3 -3
- package/src/zalo-js.credentials.test.ts +465 -0
- package/src/zalo-js.test-mocks.ts +89 -0
- package/src/zalo-js.ts +491 -274
- package/src/zca-client.test.ts +24 -0
- package/src/zca-client.ts +24 -58
- package/src/zca-constants.ts +55 -0
- package/test-api.ts +21 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/onboarding.ts +0 -340
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addWildcardAllowFrom,
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
formatCliCommand,
|
|
5
|
+
formatDocsLink,
|
|
6
|
+
formatResolvedUnresolvedNote,
|
|
7
|
+
mergeAllowFromEntries,
|
|
8
|
+
normalizeAccountId,
|
|
9
|
+
patchScopedAccountConfig,
|
|
10
|
+
type ChannelSetupDmPolicy,
|
|
11
|
+
type ChannelSetupWizard,
|
|
12
|
+
type DmPolicy,
|
|
13
|
+
type OpenClawConfig,
|
|
14
|
+
} from "openclaw/plugin-sdk/setup";
|
|
15
|
+
import {
|
|
16
|
+
listZalouserAccountIds,
|
|
17
|
+
resolveDefaultZalouserAccountId,
|
|
18
|
+
resolveZalouserAccountSync,
|
|
19
|
+
checkZcaAuthenticated,
|
|
20
|
+
} from "./accounts.js";
|
|
21
|
+
import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
|
|
22
|
+
import { zalouserSetupAdapter } from "./setup-core.js";
|
|
23
|
+
import {
|
|
24
|
+
logoutZaloProfile,
|
|
25
|
+
resolveZaloAllowFromEntries,
|
|
26
|
+
resolveZaloGroupsByEntries,
|
|
27
|
+
startZaloQrLogin,
|
|
28
|
+
waitForZaloQrLogin,
|
|
29
|
+
} from "./zalo-js.js";
|
|
30
|
+
|
|
31
|
+
const channel = "zalouser" as const;
|
|
32
|
+
const ZALOUSER_ALLOW_FROM_PLACEHOLDER = "Alice, 123456789, or leave empty to configure later";
|
|
33
|
+
const ZALOUSER_GROUPS_PLACEHOLDER = "Family, Work, 123456789, or leave empty for now";
|
|
34
|
+
const ZALOUSER_DM_ACCESS_TITLE = "Zalo Personal DM access";
|
|
35
|
+
const ZALOUSER_ALLOWLIST_TITLE = "Zalo Personal allowlist";
|
|
36
|
+
const ZALOUSER_GROUPS_TITLE = "Zalo groups";
|
|
37
|
+
|
|
38
|
+
function parseZalouserEntries(raw: string): string[] {
|
|
39
|
+
return raw
|
|
40
|
+
.split(/[\n,;]+/g)
|
|
41
|
+
.map((entry) => entry.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setZalouserAccountScopedConfig(
|
|
46
|
+
cfg: OpenClawConfig,
|
|
47
|
+
accountId: string,
|
|
48
|
+
defaultPatch: Record<string, unknown>,
|
|
49
|
+
accountPatch: Record<string, unknown> = defaultPatch,
|
|
50
|
+
): OpenClawConfig {
|
|
51
|
+
return patchScopedAccountConfig({
|
|
52
|
+
cfg,
|
|
53
|
+
channelKey: channel,
|
|
54
|
+
accountId,
|
|
55
|
+
patch: defaultPatch,
|
|
56
|
+
accountPatch,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setZalouserDmPolicy(
|
|
61
|
+
cfg: OpenClawConfig,
|
|
62
|
+
accountId: string,
|
|
63
|
+
policy: DmPolicy,
|
|
64
|
+
): OpenClawConfig {
|
|
65
|
+
const resolvedAccountId = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
|
|
66
|
+
const resolved = resolveZalouserAccountSync({ cfg, accountId: resolvedAccountId });
|
|
67
|
+
return setZalouserAccountScopedConfig(
|
|
68
|
+
cfg,
|
|
69
|
+
resolvedAccountId,
|
|
70
|
+
{
|
|
71
|
+
dmPolicy: policy,
|
|
72
|
+
...(policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
dmPolicy: policy,
|
|
76
|
+
...(policy === "open" ? { allowFrom: addWildcardAllowFrom(resolved.config.allowFrom) } : {}),
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setZalouserGroupPolicy(
|
|
82
|
+
cfg: OpenClawConfig,
|
|
83
|
+
accountId: string,
|
|
84
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
85
|
+
): OpenClawConfig {
|
|
86
|
+
return setZalouserAccountScopedConfig(cfg, accountId, {
|
|
87
|
+
groupPolicy,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setZalouserGroupAllowlist(
|
|
92
|
+
cfg: OpenClawConfig,
|
|
93
|
+
accountId: string,
|
|
94
|
+
groupKeys: string[],
|
|
95
|
+
): OpenClawConfig {
|
|
96
|
+
const groups = Object.fromEntries(
|
|
97
|
+
groupKeys.map((key) => [key, { enabled: true, requireMention: true }]),
|
|
98
|
+
);
|
|
99
|
+
return setZalouserAccountScopedConfig(cfg, accountId, {
|
|
100
|
+
groups,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ensureZalouserPluginEnabled(cfg: OpenClawConfig): OpenClawConfig {
|
|
105
|
+
const next: OpenClawConfig = {
|
|
106
|
+
...cfg,
|
|
107
|
+
plugins: {
|
|
108
|
+
...cfg.plugins,
|
|
109
|
+
entries: {
|
|
110
|
+
...cfg.plugins?.entries,
|
|
111
|
+
zalouser: {
|
|
112
|
+
...cfg.plugins?.entries?.zalouser,
|
|
113
|
+
enabled: true,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const allow = next.plugins?.allow;
|
|
119
|
+
if (!Array.isArray(allow) || allow.includes(channel)) {
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...next,
|
|
124
|
+
plugins: {
|
|
125
|
+
...next.plugins,
|
|
126
|
+
allow: [...allow, channel],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function noteZalouserHelp(
|
|
132
|
+
prompter: Parameters<NonNullable<ChannelSetupWizard["prepare"]>>[0]["prompter"],
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
await prompter.note(
|
|
135
|
+
[
|
|
136
|
+
"Zalo Personal Account login via QR code.",
|
|
137
|
+
"",
|
|
138
|
+
"This plugin uses zca-js directly (no external CLI dependency).",
|
|
139
|
+
"",
|
|
140
|
+
`Docs: ${formatDocsLink("/channels/zalouser", "zalouser")}`,
|
|
141
|
+
].join("\n"),
|
|
142
|
+
"Zalo Personal Setup",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function promptZalouserAllowFrom(params: {
|
|
147
|
+
cfg: OpenClawConfig;
|
|
148
|
+
prompter: Parameters<NonNullable<ChannelSetupDmPolicy["promptAllowFrom"]>>[0]["prompter"];
|
|
149
|
+
accountId: string;
|
|
150
|
+
}): Promise<OpenClawConfig> {
|
|
151
|
+
const { cfg, prompter, accountId } = params;
|
|
152
|
+
const resolved = resolveZalouserAccountSync({ cfg, accountId });
|
|
153
|
+
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
|
154
|
+
|
|
155
|
+
while (true) {
|
|
156
|
+
const entry = await prompter.text({
|
|
157
|
+
message: "Zalouser allowFrom (name or user id)",
|
|
158
|
+
placeholder: ZALOUSER_ALLOW_FROM_PLACEHOLDER,
|
|
159
|
+
initialValue: existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : undefined,
|
|
160
|
+
});
|
|
161
|
+
const parts = parseZalouserEntries(entry);
|
|
162
|
+
if (parts.length === 0) {
|
|
163
|
+
await prompter.note(
|
|
164
|
+
[
|
|
165
|
+
"No DM allowlist entries added yet.",
|
|
166
|
+
"Direct chats will stay blocked until you add people later.",
|
|
167
|
+
`Tip: use \`${formatCliCommand("openclaw directory peers list --channel zalouser")}\` to look up people after onboarding.`,
|
|
168
|
+
].join("\n"),
|
|
169
|
+
ZALOUSER_ALLOWLIST_TITLE,
|
|
170
|
+
);
|
|
171
|
+
return setZalouserAccountScopedConfig(cfg, accountId, {
|
|
172
|
+
dmPolicy: "allowlist",
|
|
173
|
+
allowFrom: [],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const resolvedEntries = await resolveZaloAllowFromEntries({
|
|
177
|
+
profile: resolved.profile,
|
|
178
|
+
entries: parts,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const unresolved = resolvedEntries.filter((item) => !item.resolved).map((item) => item.input);
|
|
182
|
+
if (unresolved.length > 0) {
|
|
183
|
+
await prompter.note(
|
|
184
|
+
`Could not resolve: ${unresolved.join(", ")}. Use numeric user ids or exact friend names.`,
|
|
185
|
+
ZALOUSER_ALLOWLIST_TITLE,
|
|
186
|
+
);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const resolvedIds = resolvedEntries
|
|
191
|
+
.filter((item) => item.resolved && item.id)
|
|
192
|
+
.map((item) => item.id as string);
|
|
193
|
+
const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
|
|
194
|
+
|
|
195
|
+
const notes = resolvedEntries
|
|
196
|
+
.filter((item) => item.note)
|
|
197
|
+
.map((item) => `${item.input} -> ${item.id} (${item.note})`);
|
|
198
|
+
if (notes.length > 0) {
|
|
199
|
+
await prompter.note(notes.join("\n"), ZALOUSER_ALLOWLIST_TITLE);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return setZalouserAccountScopedConfig(cfg, accountId, {
|
|
203
|
+
dmPolicy: "allowlist",
|
|
204
|
+
allowFrom: unique,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const zalouserDmPolicy: ChannelSetupDmPolicy = {
|
|
210
|
+
label: "Zalo Personal",
|
|
211
|
+
channel,
|
|
212
|
+
policyKey: "channels.zalouser.dmPolicy",
|
|
213
|
+
allowFromKey: "channels.zalouser.allowFrom",
|
|
214
|
+
resolveConfigKeys: (cfg, accountId) =>
|
|
215
|
+
(accountId ?? resolveDefaultZalouserAccountId(cfg)) !== DEFAULT_ACCOUNT_ID
|
|
216
|
+
? {
|
|
217
|
+
policyKey: `channels.zalouser.accounts.${accountId ?? resolveDefaultZalouserAccountId(cfg)}.dmPolicy`,
|
|
218
|
+
allowFromKey: `channels.zalouser.accounts.${accountId ?? resolveDefaultZalouserAccountId(cfg)}.allowFrom`,
|
|
219
|
+
}
|
|
220
|
+
: {
|
|
221
|
+
policyKey: "channels.zalouser.dmPolicy",
|
|
222
|
+
allowFromKey: "channels.zalouser.allowFrom",
|
|
223
|
+
},
|
|
224
|
+
getCurrent: (cfg, accountId) =>
|
|
225
|
+
resolveZalouserAccountSync({
|
|
226
|
+
cfg,
|
|
227
|
+
accountId: accountId ?? resolveDefaultZalouserAccountId(cfg),
|
|
228
|
+
}).config.dmPolicy ?? "pairing",
|
|
229
|
+
setPolicy: (cfg, policy, accountId) =>
|
|
230
|
+
setZalouserDmPolicy(cfg, accountId ?? resolveDefaultZalouserAccountId(cfg), policy),
|
|
231
|
+
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
|
232
|
+
const id =
|
|
233
|
+
accountId && normalizeAccountId(accountId)
|
|
234
|
+
? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
|
|
235
|
+
: resolveDefaultZalouserAccountId(cfg);
|
|
236
|
+
return await promptZalouserAllowFrom({
|
|
237
|
+
cfg: cfg,
|
|
238
|
+
prompter,
|
|
239
|
+
accountId: id,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
async function promptZalouserQuickstartDmPolicy(params: {
|
|
245
|
+
cfg: OpenClawConfig;
|
|
246
|
+
prompter: Parameters<NonNullable<ChannelSetupWizard["prepare"]>>[0]["prompter"];
|
|
247
|
+
accountId: string;
|
|
248
|
+
}): Promise<OpenClawConfig> {
|
|
249
|
+
const { cfg, prompter, accountId } = params;
|
|
250
|
+
const resolved = resolveZalouserAccountSync({ cfg, accountId });
|
|
251
|
+
const existingPolicy = resolved.config.dmPolicy ?? "pairing";
|
|
252
|
+
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
|
253
|
+
const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
|
|
254
|
+
|
|
255
|
+
await prompter.note(
|
|
256
|
+
[
|
|
257
|
+
"Direct chats are configured separately from group chats.",
|
|
258
|
+
"- pairing (default): unknown people get a pairing code",
|
|
259
|
+
"- allowlist: only listed people can DM",
|
|
260
|
+
"- open: anyone can DM",
|
|
261
|
+
"- disabled: ignore DMs",
|
|
262
|
+
"",
|
|
263
|
+
`Current: dmPolicy=${existingPolicy}, allowFrom=${existingLabel}`,
|
|
264
|
+
"If you choose allowlist now, you can leave it empty and add people later.",
|
|
265
|
+
].join("\n"),
|
|
266
|
+
ZALOUSER_DM_ACCESS_TITLE,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const policy = (await prompter.select({
|
|
270
|
+
message: "Zalo Personal DM policy",
|
|
271
|
+
options: [
|
|
272
|
+
{ value: "pairing", label: "Pairing (recommended)" },
|
|
273
|
+
{ value: "allowlist", label: "Allowlist (specific users only)" },
|
|
274
|
+
{ value: "open", label: "Open (public inbound DMs)" },
|
|
275
|
+
{ value: "disabled", label: "Disabled (ignore DMs)" },
|
|
276
|
+
],
|
|
277
|
+
initialValue: existingPolicy,
|
|
278
|
+
})) as DmPolicy;
|
|
279
|
+
|
|
280
|
+
if (policy === "allowlist") {
|
|
281
|
+
return await promptZalouserAllowFrom({
|
|
282
|
+
cfg,
|
|
283
|
+
prompter,
|
|
284
|
+
accountId,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return setZalouserDmPolicy(cfg, accountId, policy);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export { zalouserSetupAdapter } from "./setup-core.js";
|
|
291
|
+
|
|
292
|
+
export const zalouserSetupWizard: ChannelSetupWizard = {
|
|
293
|
+
channel,
|
|
294
|
+
status: {
|
|
295
|
+
configuredLabel: "logged in",
|
|
296
|
+
unconfiguredLabel: "needs QR login",
|
|
297
|
+
configuredHint: "recommended · logged in",
|
|
298
|
+
unconfiguredHint: "recommended · QR login",
|
|
299
|
+
configuredScore: 1,
|
|
300
|
+
unconfiguredScore: 15,
|
|
301
|
+
resolveConfigured: async ({ cfg, accountId }) => {
|
|
302
|
+
const ids = accountId ? [accountId] : listZalouserAccountIds(cfg);
|
|
303
|
+
for (const resolvedAccountId of ids) {
|
|
304
|
+
const account = resolveZalouserAccountSync({ cfg, accountId: resolvedAccountId });
|
|
305
|
+
if (await checkZcaAuthenticated(account.profile)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
},
|
|
311
|
+
resolveStatusLines: async ({ cfg, accountId, configured }) => {
|
|
312
|
+
void cfg;
|
|
313
|
+
const label =
|
|
314
|
+
accountId && accountId !== DEFAULT_ACCOUNT_ID
|
|
315
|
+
? `Zalo Personal (${accountId})`
|
|
316
|
+
: "Zalo Personal";
|
|
317
|
+
return [`${label}: ${configured ? "logged in" : "needs QR login"}`];
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
prepare: async ({ cfg, accountId, prompter, options }) => {
|
|
321
|
+
let next = cfg;
|
|
322
|
+
const account = resolveZalouserAccountSync({ cfg: next, accountId });
|
|
323
|
+
const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
|
|
324
|
+
|
|
325
|
+
if (!alreadyAuthenticated) {
|
|
326
|
+
await noteZalouserHelp(prompter);
|
|
327
|
+
const wantsLogin = await prompter.confirm({
|
|
328
|
+
message: "Login via QR code now?",
|
|
329
|
+
initialValue: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (wantsLogin) {
|
|
333
|
+
const start = await startZaloQrLogin({ profile: account.profile, timeoutMs: 35_000 });
|
|
334
|
+
if (start.qrDataUrl) {
|
|
335
|
+
const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
|
|
336
|
+
await prompter.note(
|
|
337
|
+
[
|
|
338
|
+
start.message,
|
|
339
|
+
qrPath
|
|
340
|
+
? `QR image saved to: ${qrPath}`
|
|
341
|
+
: "Could not write QR image file; use gateway web login UI instead.",
|
|
342
|
+
"Scan + approve on phone, then continue.",
|
|
343
|
+
].join("\n"),
|
|
344
|
+
"QR Login",
|
|
345
|
+
);
|
|
346
|
+
const scanned = await prompter.confirm({
|
|
347
|
+
message: "Did you scan and approve the QR on your phone?",
|
|
348
|
+
initialValue: true,
|
|
349
|
+
});
|
|
350
|
+
if (scanned) {
|
|
351
|
+
const waited = await waitForZaloQrLogin({
|
|
352
|
+
profile: account.profile,
|
|
353
|
+
timeoutMs: 120_000,
|
|
354
|
+
});
|
|
355
|
+
await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
await prompter.note(start.message, "Login pending");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
const keepSession = await prompter.confirm({
|
|
363
|
+
message: "Zalo Personal already logged in. Keep session?",
|
|
364
|
+
initialValue: true,
|
|
365
|
+
});
|
|
366
|
+
if (!keepSession) {
|
|
367
|
+
await logoutZaloProfile(account.profile);
|
|
368
|
+
const start = await startZaloQrLogin({
|
|
369
|
+
profile: account.profile,
|
|
370
|
+
force: true,
|
|
371
|
+
timeoutMs: 35_000,
|
|
372
|
+
});
|
|
373
|
+
if (start.qrDataUrl) {
|
|
374
|
+
const qrPath = await writeQrDataUrlToTempFile(start.qrDataUrl, account.profile);
|
|
375
|
+
await prompter.note(
|
|
376
|
+
[start.message, qrPath ? `QR image saved to: ${qrPath}` : undefined]
|
|
377
|
+
.filter(Boolean)
|
|
378
|
+
.join("\n"),
|
|
379
|
+
"QR Login",
|
|
380
|
+
);
|
|
381
|
+
const waited = await waitForZaloQrLogin({ profile: account.profile, timeoutMs: 120_000 });
|
|
382
|
+
await prompter.note(waited.message, waited.connected ? "Success" : "Login pending");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
next = setZalouserAccountScopedConfig(
|
|
388
|
+
next,
|
|
389
|
+
accountId,
|
|
390
|
+
{ profile: account.profile !== "default" ? account.profile : undefined },
|
|
391
|
+
{ profile: account.profile, enabled: true },
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (options?.quickstartDefaults) {
|
|
395
|
+
next = await promptZalouserQuickstartDmPolicy({
|
|
396
|
+
cfg: next,
|
|
397
|
+
prompter,
|
|
398
|
+
accountId,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { cfg: next };
|
|
403
|
+
},
|
|
404
|
+
credentials: [],
|
|
405
|
+
groupAccess: {
|
|
406
|
+
label: "Zalo groups",
|
|
407
|
+
placeholder: ZALOUSER_GROUPS_PLACEHOLDER,
|
|
408
|
+
currentPolicy: ({ cfg, accountId }) =>
|
|
409
|
+
resolveZalouserAccountSync({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
|
410
|
+
currentEntries: ({ cfg, accountId }) =>
|
|
411
|
+
Object.keys(resolveZalouserAccountSync({ cfg, accountId }).config.groups ?? {}),
|
|
412
|
+
updatePrompt: ({ cfg, accountId }) =>
|
|
413
|
+
Boolean(resolveZalouserAccountSync({ cfg, accountId }).config.groups),
|
|
414
|
+
setPolicy: ({ cfg, accountId, policy }) => setZalouserGroupPolicy(cfg, accountId, policy),
|
|
415
|
+
resolveAllowlist: async ({ cfg, accountId, entries, prompter }) => {
|
|
416
|
+
if (entries.length === 0) {
|
|
417
|
+
await prompter.note(
|
|
418
|
+
[
|
|
419
|
+
"No group allowlist entries added yet.",
|
|
420
|
+
"Group chats will stay blocked until you add groups later.",
|
|
421
|
+
`Tip: use \`${formatCliCommand("openclaw directory groups list --channel zalouser")}\` after onboarding to find group IDs.`,
|
|
422
|
+
"Mention requirement stays on by default for groups you allow later.",
|
|
423
|
+
].join("\n"),
|
|
424
|
+
ZALOUSER_GROUPS_TITLE,
|
|
425
|
+
);
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const updatedAccount = resolveZalouserAccountSync({ cfg: cfg, accountId });
|
|
429
|
+
try {
|
|
430
|
+
const resolved = await resolveZaloGroupsByEntries({
|
|
431
|
+
profile: updatedAccount.profile,
|
|
432
|
+
entries,
|
|
433
|
+
});
|
|
434
|
+
const resolvedIds = resolved
|
|
435
|
+
.filter((entry) => entry.resolved && entry.id)
|
|
436
|
+
.map((entry) => entry.id as string);
|
|
437
|
+
const unresolved = resolved.filter((entry) => !entry.resolved).map((entry) => entry.input);
|
|
438
|
+
const keys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
|
439
|
+
const resolution = formatResolvedUnresolvedNote({
|
|
440
|
+
resolved: resolvedIds,
|
|
441
|
+
unresolved,
|
|
442
|
+
});
|
|
443
|
+
if (resolution) {
|
|
444
|
+
await prompter.note(resolution, ZALOUSER_GROUPS_TITLE);
|
|
445
|
+
}
|
|
446
|
+
return keys;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
await prompter.note(
|
|
449
|
+
`Group lookup failed; keeping entries as typed. ${String(err)}`,
|
|
450
|
+
ZALOUSER_GROUPS_TITLE,
|
|
451
|
+
);
|
|
452
|
+
return entries.map((entry) => entry.trim()).filter(Boolean);
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
applyAllowlist: ({ cfg, accountId, resolved }) =>
|
|
456
|
+
setZalouserGroupAllowlist(cfg, accountId, resolved as string[]),
|
|
457
|
+
},
|
|
458
|
+
finalize: async ({ cfg, accountId, forceAllowFrom, options, prompter }) => {
|
|
459
|
+
let next = cfg;
|
|
460
|
+
if (forceAllowFrom && !options?.quickstartDefaults) {
|
|
461
|
+
next = await promptZalouserAllowFrom({
|
|
462
|
+
cfg: next,
|
|
463
|
+
prompter,
|
|
464
|
+
accountId,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return { cfg: ensureZalouserPluginEnabled(next) };
|
|
468
|
+
},
|
|
469
|
+
dmPolicy: zalouserDmPolicy,
|
|
470
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
2
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
3
|
+
import {
|
|
4
|
+
listZalouserAccountIds,
|
|
5
|
+
resolveDefaultZalouserAccountId,
|
|
6
|
+
resolveZalouserAccountSync,
|
|
7
|
+
} from "./accounts.js";
|
|
8
|
+
import { zalouserSetupAdapter } from "./setup-core.js";
|
|
9
|
+
import { zalouserSetupWizard } from "./setup-surface.js";
|
|
10
|
+
|
|
11
|
+
export const zalouserSetupPlugin = {
|
|
12
|
+
id: "zalouser",
|
|
13
|
+
meta: {
|
|
14
|
+
id: "zalouser",
|
|
15
|
+
label: "ZaloUser",
|
|
16
|
+
selectionLabel: "ZaloUser",
|
|
17
|
+
docsPath: "/channels/zalouser",
|
|
18
|
+
blurb: "Unofficial Zalo personal account connector.",
|
|
19
|
+
},
|
|
20
|
+
capabilities: {
|
|
21
|
+
chatTypes: ["direct", "group"] as Array<"direct" | "group">,
|
|
22
|
+
},
|
|
23
|
+
config: {
|
|
24
|
+
listAccountIds: (cfg: unknown) => listZalouserAccountIds(cfg as never),
|
|
25
|
+
defaultAccountId: (cfg: unknown) => resolveDefaultZalouserAccountId(cfg as never),
|
|
26
|
+
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
|
|
27
|
+
resolveZalouserAccountSync({ cfg, accountId }),
|
|
28
|
+
},
|
|
29
|
+
security: {
|
|
30
|
+
resolveDmPolicy: createScopedDmSecurityResolver({
|
|
31
|
+
channelKey: "zalouser",
|
|
32
|
+
resolvePolicy: (account: ReturnType<typeof resolveZalouserAccountSync>) =>
|
|
33
|
+
account.config.dmPolicy,
|
|
34
|
+
resolveAllowFrom: (account: ReturnType<typeof resolveZalouserAccountSync>) =>
|
|
35
|
+
account.config.allowFrom,
|
|
36
|
+
policyPathSuffix: "dmPolicy",
|
|
37
|
+
normalizeEntry: (raw: string) => raw.trim().replace(/^(zalouser|zlu):/i, ""),
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
setup: zalouserSetupAdapter,
|
|
41
|
+
setupWizard: zalouserSetupWizard,
|
|
42
|
+
} as const;
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
2
|
+
import {
|
|
3
|
+
adaptScopedAccountAccessor,
|
|
4
|
+
createScopedChannelConfigAdapter,
|
|
5
|
+
} from "openclaw/plugin-sdk/channel-config-helpers";
|
|
6
|
+
import {
|
|
7
|
+
listZalouserAccountIds,
|
|
8
|
+
resolveDefaultZalouserAccountId,
|
|
9
|
+
resolveZalouserAccountSync,
|
|
10
|
+
checkZcaAuthenticated,
|
|
11
|
+
type ResolvedZalouserAccount,
|
|
12
|
+
} from "./accounts.js";
|
|
13
|
+
import type { ChannelPlugin } from "./channel-api.js";
|
|
14
|
+
import { buildChannelConfigSchema, formatAllowFromLowercase } from "./channel-api.js";
|
|
15
|
+
import { ZalouserConfigSchema } from "./config-schema.js";
|
|
16
|
+
import { zalouserDoctor } from "./doctor.js";
|
|
17
|
+
|
|
18
|
+
const zalouserMeta: ChannelPlugin<ResolvedZalouserAccount>["meta"] = {
|
|
19
|
+
id: "zalouser",
|
|
20
|
+
label: "Zalo Personal",
|
|
21
|
+
selectionLabel: "Zalo (Personal Account)",
|
|
22
|
+
docsPath: "/channels/zalouser",
|
|
23
|
+
docsLabel: "zalouser",
|
|
24
|
+
blurb: "Zalo personal account via QR code login.",
|
|
25
|
+
aliases: ["zlu"],
|
|
26
|
+
order: 85,
|
|
27
|
+
quickstartAllowFrom: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const zalouserConfigAdapter = createScopedChannelConfigAdapter<ResolvedZalouserAccount>({
|
|
31
|
+
sectionKey: "zalouser",
|
|
32
|
+
listAccountIds: listZalouserAccountIds,
|
|
33
|
+
resolveAccount: adaptScopedAccountAccessor(resolveZalouserAccountSync),
|
|
34
|
+
defaultAccountId: resolveDefaultZalouserAccountId,
|
|
35
|
+
clearBaseFields: [
|
|
36
|
+
"profile",
|
|
37
|
+
"name",
|
|
38
|
+
"dmPolicy",
|
|
39
|
+
"allowFrom",
|
|
40
|
+
"historyLimit",
|
|
41
|
+
"groupAllowFrom",
|
|
42
|
+
"groupPolicy",
|
|
43
|
+
"groups",
|
|
44
|
+
"messagePrefix",
|
|
45
|
+
],
|
|
46
|
+
resolveAllowFrom: (account) => account.config.allowFrom,
|
|
47
|
+
formatAllowFrom: (allowFrom) =>
|
|
48
|
+
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export function createZalouserPluginBase(params: {
|
|
52
|
+
setupWizard: NonNullable<ChannelPlugin<ResolvedZalouserAccount>["setupWizard"]>;
|
|
53
|
+
setup: NonNullable<ChannelPlugin<ResolvedZalouserAccount>["setup"]>;
|
|
54
|
+
}): Pick<
|
|
55
|
+
ChannelPlugin<ResolvedZalouserAccount>,
|
|
56
|
+
| "id"
|
|
57
|
+
| "meta"
|
|
58
|
+
| "setupWizard"
|
|
59
|
+
| "capabilities"
|
|
60
|
+
| "doctor"
|
|
61
|
+
| "reload"
|
|
62
|
+
| "configSchema"
|
|
63
|
+
| "config"
|
|
64
|
+
| "setup"
|
|
65
|
+
> {
|
|
66
|
+
return {
|
|
67
|
+
id: "zalouser",
|
|
68
|
+
meta: zalouserMeta,
|
|
69
|
+
setupWizard: params.setupWizard,
|
|
70
|
+
capabilities: {
|
|
71
|
+
chatTypes: ["direct", "group"],
|
|
72
|
+
media: true,
|
|
73
|
+
reactions: true,
|
|
74
|
+
threads: false,
|
|
75
|
+
polls: false,
|
|
76
|
+
nativeCommands: false,
|
|
77
|
+
blockStreaming: true,
|
|
78
|
+
},
|
|
79
|
+
doctor: zalouserDoctor,
|
|
80
|
+
reload: { configPrefixes: ["channels.zalouser"] },
|
|
81
|
+
configSchema: buildChannelConfigSchema(ZalouserConfigSchema),
|
|
82
|
+
config: {
|
|
83
|
+
...zalouserConfigAdapter,
|
|
84
|
+
isConfigured: async (account) => await checkZcaAuthenticated(account.profile),
|
|
85
|
+
describeAccount: (account) =>
|
|
86
|
+
describeAccountSnapshot({
|
|
87
|
+
account,
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
setup: params.setup,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { expectOpenDmPolicyConfigIssue } from "openclaw/plugin-sdk/channel-test-helpers";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { expectOpenDmPolicyConfigIssue } from "../../test-utils/status-issues.js";
|
|
3
3
|
import { collectZalouserStatusIssues } from "./status-issues.js";
|
|
4
4
|
|
|
5
5
|
describe("collectZalouserStatusIssues", () => {
|
|
@@ -28,16 +28,4 @@ describe("collectZalouserStatusIssues", () => {
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
|
-
|
|
32
|
-
it("skips disabled accounts", () => {
|
|
33
|
-
const issues = collectZalouserStatusIssues([
|
|
34
|
-
{
|
|
35
|
-
accountId: "default",
|
|
36
|
-
enabled: false,
|
|
37
|
-
configured: false,
|
|
38
|
-
lastError: "not authenticated",
|
|
39
|
-
},
|
|
40
|
-
]);
|
|
41
|
-
expect(issues).toHaveLength(0);
|
|
42
|
-
});
|
|
43
31
|
});
|
package/src/status-issues.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ChannelAccountSnapshot,
|
|
3
|
+
ChannelStatusIssue,
|
|
4
|
+
} from "openclaw/plugin-sdk/channel-contract";
|
|
5
|
+
import {
|
|
6
|
+
coerceStatusIssueAccountId,
|
|
7
|
+
readStatusIssueFields,
|
|
8
|
+
} from "openclaw/plugin-sdk/extension-shared";
|
|
3
9
|
|
|
4
10
|
const ZALOUSER_STATUS_FIELDS = [
|
|
5
11
|
"accountId",
|
package/src/test-helpers.ts
CHANGED
package/src/text-styles.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { parseZalouserTextStyles } from "./text-styles.js";
|
|
3
|
-
import { TextStyle } from "./zca-
|
|
3
|
+
import { TextStyle } from "./zca-constants.js";
|
|
4
4
|
|
|
5
5
|
describe("parseZalouserTextStyles", () => {
|
|
6
6
|
it("renders inline markdown emphasis as Zalo style ranges", () => {
|
package/src/text-styles.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { TextStyle, type Style } from "./zca-
|
|
1
|
+
import { TextStyle, type Style } from "./zca-constants.js";
|
|
2
|
+
|
|
3
|
+
const ESCAPE_SENTINEL_START = "\u0001";
|
|
4
|
+
const ESCAPE_SENTINEL_END = "\u0002";
|
|
2
5
|
|
|
3
6
|
type InlineStyle = (typeof TextStyle)[keyof typeof TextStyle];
|
|
4
7
|
|
|
@@ -262,7 +265,7 @@ export function parseZalouserTextStyles(input: string): { text: string; styles:
|
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
if (escapeMap.length > 0) {
|
|
265
|
-
const escapeRegex =
|
|
268
|
+
const escapeRegex = new RegExp(`${ESCAPE_SENTINEL_START}(\\d+)${ESCAPE_SENTINEL_END}`, "g");
|
|
266
269
|
const shifts: Array<{ pos: number; delta: number }> = [];
|
|
267
270
|
let cumulativeDelta = 0;
|
|
268
271
|
|