@clawdbot/zalouser 2026.1.16 → 2026.1.21
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 +23 -0
- package/clawdbot.plugin.json +11 -0
- package/index.ts +7 -3
- package/package.json +21 -2
- package/src/accounts.ts +17 -21
- package/src/channel.ts +196 -103
- package/src/config-schema.ts +24 -0
- package/src/monitor.ts +229 -46
- package/src/onboarding.ts +259 -83
- package/src/probe.ts +28 -0
- package/src/runtime.ts +14 -0
- package/src/status-issues.test.ts +58 -0
- package/src/status-issues.ts +81 -0
- package/src/types.ts +4 -11
- package/src/zca.ts +26 -1
- package/src/core-bridge.ts +0 -171
package/src/onboarding.ts
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ChannelOnboardingAdapter,
|
|
3
|
+
ChannelOnboardingDmPolicy,
|
|
4
|
+
ClawdbotConfig,
|
|
5
|
+
WizardPrompter,
|
|
6
|
+
} from "clawdbot/plugin-sdk";
|
|
7
|
+
import {
|
|
8
|
+
addWildcardAllowFrom,
|
|
9
|
+
DEFAULT_ACCOUNT_ID,
|
|
10
|
+
normalizeAccountId,
|
|
11
|
+
promptAccountId,
|
|
12
|
+
promptChannelAccessConfig,
|
|
13
|
+
} from "clawdbot/plugin-sdk";
|
|
3
14
|
|
|
4
15
|
import {
|
|
5
16
|
listZalouserAccountIds,
|
|
6
17
|
resolveDefaultZalouserAccountId,
|
|
7
18
|
resolveZalouserAccountSync,
|
|
8
|
-
normalizeAccountId,
|
|
9
19
|
checkZcaAuthenticated,
|
|
10
20
|
} from "./accounts.js";
|
|
11
|
-
import { runZcaInteractive, checkZcaInstalled } from "./zca.js";
|
|
12
|
-
import {
|
|
21
|
+
import { runZca, runZcaInteractive, checkZcaInstalled, parseJsonOutput } from "./zca.js";
|
|
22
|
+
import type { ZcaFriend, ZcaGroup } from "./types.js";
|
|
13
23
|
|
|
14
24
|
const channel = "zalouser" as const;
|
|
15
25
|
|
|
16
26
|
function setZalouserDmPolicy(
|
|
17
|
-
cfg:
|
|
27
|
+
cfg: ClawdbotConfig,
|
|
18
28
|
dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
|
|
19
|
-
):
|
|
29
|
+
): ClawdbotConfig {
|
|
20
30
|
const allowFrom =
|
|
21
31
|
dmPolicy === "open"
|
|
22
|
-
?
|
|
23
|
-
(v, i, a) => a.indexOf(v) === i,
|
|
24
|
-
)
|
|
32
|
+
? addWildcardAllowFrom(cfg.channels?.zalouser?.allowFrom)
|
|
25
33
|
: undefined;
|
|
26
34
|
return {
|
|
27
35
|
...cfg,
|
|
@@ -33,7 +41,7 @@ function setZalouserDmPolicy(
|
|
|
33
41
|
...(allowFrom ? { allowFrom } : {}),
|
|
34
42
|
},
|
|
35
43
|
},
|
|
36
|
-
} as
|
|
44
|
+
} as ClawdbotConfig;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
|
|
@@ -52,32 +60,80 @@ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
|
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
async function promptZalouserAllowFrom(params: {
|
|
55
|
-
cfg:
|
|
63
|
+
cfg: ClawdbotConfig;
|
|
56
64
|
prompter: WizardPrompter;
|
|
57
65
|
accountId: string;
|
|
58
|
-
}): Promise<
|
|
66
|
+
}): Promise<ClawdbotConfig> {
|
|
59
67
|
const { cfg, prompter, accountId } = params;
|
|
60
68
|
const resolved = resolveZalouserAccountSync({ cfg, accountId });
|
|
61
69
|
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 ClawdbotConfig;
|
|
135
|
+
}
|
|
79
136
|
|
|
80
|
-
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
81
137
|
return {
|
|
82
138
|
...cfg,
|
|
83
139
|
channels: {
|
|
@@ -85,13 +141,39 @@ async function promptZalouserAllowFrom(params: {
|
|
|
85
141
|
zalouser: {
|
|
86
142
|
...cfg.channels?.zalouser,
|
|
87
143
|
enabled: true,
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
},
|
|
90
153
|
},
|
|
91
154
|
},
|
|
92
|
-
} as
|
|
155
|
+
} as ClawdbotConfig;
|
|
93
156
|
}
|
|
157
|
+
}
|
|
94
158
|
|
|
159
|
+
function setZalouserGroupPolicy(
|
|
160
|
+
cfg: ClawdbotConfig,
|
|
161
|
+
accountId: string,
|
|
162
|
+
groupPolicy: "open" | "allowlist" | "disabled",
|
|
163
|
+
): ClawdbotConfig {
|
|
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 ClawdbotConfig;
|
|
176
|
+
}
|
|
95
177
|
return {
|
|
96
178
|
...cfg,
|
|
97
179
|
channels: {
|
|
@@ -104,55 +186,83 @@ async function promptZalouserAllowFrom(params: {
|
|
|
104
186
|
[accountId]: {
|
|
105
187
|
...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
|
|
106
188
|
enabled: cfg.channels?.zalouser?.accounts?.[accountId]?.enabled ?? true,
|
|
107
|
-
|
|
108
|
-
allowFrom: unique,
|
|
189
|
+
groupPolicy,
|
|
109
190
|
},
|
|
110
191
|
},
|
|
111
192
|
},
|
|
112
193
|
},
|
|
113
|
-
} as
|
|
194
|
+
} as ClawdbotConfig;
|
|
114
195
|
}
|
|
115
196
|
|
|
116
|
-
|
|
117
|
-
cfg:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
const selected = await prompter.select({
|
|
135
|
-
message: `${label} account`,
|
|
136
|
-
options,
|
|
137
|
-
initialValue: currentId,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (selected === "__new__") {
|
|
141
|
-
const newId = await prompter.text({
|
|
142
|
-
message: "New account ID",
|
|
143
|
-
placeholder: "work",
|
|
144
|
-
validate: (value) => {
|
|
145
|
-
const raw = String(value ?? "").trim().toLowerCase();
|
|
146
|
-
if (!raw) return "Required";
|
|
147
|
-
if (!/^[a-z0-9_-]+$/.test(raw)) return "Use lowercase alphanumeric, dash, or underscore";
|
|
148
|
-
if (existingIds.includes(raw)) return "Account already exists";
|
|
149
|
-
return undefined;
|
|
197
|
+
function setZalouserGroupAllowlist(
|
|
198
|
+
cfg: ClawdbotConfig,
|
|
199
|
+
accountId: string,
|
|
200
|
+
groupKeys: string[],
|
|
201
|
+
): ClawdbotConfig {
|
|
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
|
+
},
|
|
150
213
|
},
|
|
151
|
-
}
|
|
152
|
-
|
|
214
|
+
} as ClawdbotConfig;
|
|
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 ClawdbotConfig;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function resolveZalouserGroups(params: {
|
|
237
|
+
cfg: ClawdbotConfig;
|
|
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);
|
|
153
254
|
}
|
|
154
255
|
|
|
155
|
-
return
|
|
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
|
+
});
|
|
156
266
|
}
|
|
157
267
|
|
|
158
268
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
@@ -160,18 +270,29 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
160
270
|
channel,
|
|
161
271
|
policyKey: "channels.zalouser.dmPolicy",
|
|
162
272
|
allowFromKey: "channels.zalouser.allowFrom",
|
|
163
|
-
getCurrent: (cfg) => ((cfg as
|
|
164
|
-
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as
|
|
273
|
+
getCurrent: (cfg) => ((cfg as ClawdbotConfig).channels?.zalouser?.dmPolicy ?? "pairing") as "pairing",
|
|
274
|
+
setPolicy: (cfg, policy) => setZalouserDmPolicy(cfg as ClawdbotConfig, policy),
|
|
275
|
+
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
|
276
|
+
const id =
|
|
277
|
+
accountId && normalizeAccountId(accountId)
|
|
278
|
+
? normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID
|
|
279
|
+
: resolveDefaultZalouserAccountId(cfg as ClawdbotConfig);
|
|
280
|
+
return promptZalouserAllowFrom({
|
|
281
|
+
cfg: cfg as ClawdbotConfig,
|
|
282
|
+
prompter,
|
|
283
|
+
accountId: id,
|
|
284
|
+
});
|
|
285
|
+
},
|
|
165
286
|
};
|
|
166
287
|
|
|
167
288
|
export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
168
289
|
channel,
|
|
169
290
|
dmPolicy,
|
|
170
291
|
getStatus: async ({ cfg }) => {
|
|
171
|
-
const ids = listZalouserAccountIds(cfg as
|
|
292
|
+
const ids = listZalouserAccountIds(cfg as ClawdbotConfig);
|
|
172
293
|
let configured = false;
|
|
173
294
|
for (const accountId of ids) {
|
|
174
|
-
const account = resolveZalouserAccountSync({ cfg: cfg as
|
|
295
|
+
const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
|
|
175
296
|
const isAuth = await checkZcaAuthenticated(account.profile);
|
|
176
297
|
if (isAuth) {
|
|
177
298
|
configured = true;
|
|
@@ -203,14 +324,14 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
203
324
|
}
|
|
204
325
|
|
|
205
326
|
const zalouserOverride = accountOverrides.zalouser?.trim();
|
|
206
|
-
const defaultAccountId = resolveDefaultZalouserAccountId(cfg as
|
|
327
|
+
const defaultAccountId = resolveDefaultZalouserAccountId(cfg as ClawdbotConfig);
|
|
207
328
|
let accountId = zalouserOverride
|
|
208
329
|
? normalizeAccountId(zalouserOverride)
|
|
209
330
|
: defaultAccountId;
|
|
210
331
|
|
|
211
332
|
if (shouldPromptAccountIds && !zalouserOverride) {
|
|
212
333
|
accountId = await promptAccountId({
|
|
213
|
-
cfg: cfg as
|
|
334
|
+
cfg: cfg as ClawdbotConfig,
|
|
214
335
|
prompter,
|
|
215
336
|
label: "Zalo Personal",
|
|
216
337
|
currentId: accountId,
|
|
@@ -219,7 +340,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
219
340
|
});
|
|
220
341
|
}
|
|
221
342
|
|
|
222
|
-
let next = cfg as
|
|
343
|
+
let next = cfg as ClawdbotConfig;
|
|
223
344
|
const account = resolveZalouserAccountSync({ cfg: next, accountId });
|
|
224
345
|
const alreadyAuthenticated = await checkZcaAuthenticated(account.profile);
|
|
225
346
|
|
|
@@ -277,7 +398,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
277
398
|
profile: account.profile !== "default" ? account.profile : undefined,
|
|
278
399
|
},
|
|
279
400
|
},
|
|
280
|
-
} as
|
|
401
|
+
} as ClawdbotConfig;
|
|
281
402
|
} else {
|
|
282
403
|
next = {
|
|
283
404
|
...next,
|
|
@@ -296,7 +417,7 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
296
417
|
},
|
|
297
418
|
},
|
|
298
419
|
},
|
|
299
|
-
} as
|
|
420
|
+
} as ClawdbotConfig;
|
|
300
421
|
}
|
|
301
422
|
|
|
302
423
|
if (forceAllowFrom) {
|
|
@@ -307,6 +428,61 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
307
428
|
});
|
|
308
429
|
}
|
|
309
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
|
+
|
|
310
486
|
return { cfg: next, accountId };
|
|
311
487
|
},
|
|
312
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 "clawdbot/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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { collectZalouserStatusIssues } from "./status-issues.js";
|
|
4
|
+
|
|
5
|
+
describe("collectZalouserStatusIssues", () => {
|
|
6
|
+
it("flags missing zca when configured is false", () => {
|
|
7
|
+
const issues = collectZalouserStatusIssues([
|
|
8
|
+
{
|
|
9
|
+
accountId: "default",
|
|
10
|
+
enabled: true,
|
|
11
|
+
configured: false,
|
|
12
|
+
lastError: "zca CLI not found in PATH",
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
expect(issues).toHaveLength(1);
|
|
16
|
+
expect(issues[0]?.kind).toBe("runtime");
|
|
17
|
+
expect(issues[0]?.message).toMatch(/zca CLI not found/i);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("flags missing auth when configured is false", () => {
|
|
21
|
+
const issues = collectZalouserStatusIssues([
|
|
22
|
+
{
|
|
23
|
+
accountId: "default",
|
|
24
|
+
enabled: true,
|
|
25
|
+
configured: false,
|
|
26
|
+
lastError: "not authenticated",
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
expect(issues).toHaveLength(1);
|
|
30
|
+
expect(issues[0]?.kind).toBe("auth");
|
|
31
|
+
expect(issues[0]?.message).toMatch(/Not authenticated/i);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("warns when dmPolicy is open", () => {
|
|
35
|
+
const issues = collectZalouserStatusIssues([
|
|
36
|
+
{
|
|
37
|
+
accountId: "default",
|
|
38
|
+
enabled: true,
|
|
39
|
+
configured: true,
|
|
40
|
+
dmPolicy: "open",
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
expect(issues).toHaveLength(1);
|
|
44
|
+
expect(issues[0]?.kind).toBe("config");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("skips disabled accounts", () => {
|
|
48
|
+
const issues = collectZalouserStatusIssues([
|
|
49
|
+
{
|
|
50
|
+
accountId: "default",
|
|
51
|
+
enabled: false,
|
|
52
|
+
configured: false,
|
|
53
|
+
lastError: "zca CLI not found in PATH",
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
expect(issues).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ChannelAccountSnapshot, ChannelStatusIssue } from "clawdbot/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
type ZalouserAccountStatus = {
|
|
4
|
+
accountId?: unknown;
|
|
5
|
+
enabled?: unknown;
|
|
6
|
+
configured?: unknown;
|
|
7
|
+
dmPolicy?: unknown;
|
|
8
|
+
lastError?: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
|
+
Boolean(value && typeof value === "object");
|
|
13
|
+
|
|
14
|
+
const asString = (value: unknown): string | undefined =>
|
|
15
|
+
typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined;
|
|
16
|
+
|
|
17
|
+
function readZalouserAccountStatus(value: ChannelAccountSnapshot): ZalouserAccountStatus | null {
|
|
18
|
+
if (!isRecord(value)) return null;
|
|
19
|
+
return {
|
|
20
|
+
accountId: value.accountId,
|
|
21
|
+
enabled: value.enabled,
|
|
22
|
+
configured: value.configured,
|
|
23
|
+
dmPolicy: value.dmPolicy,
|
|
24
|
+
lastError: value.lastError,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isMissingZca(lastError?: string): boolean {
|
|
29
|
+
if (!lastError) return false;
|
|
30
|
+
const lower = lastError.toLowerCase();
|
|
31
|
+
return lower.includes("zca") && (lower.includes("not found") || lower.includes("enoent"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function collectZalouserStatusIssues(
|
|
35
|
+
accounts: ChannelAccountSnapshot[],
|
|
36
|
+
): ChannelStatusIssue[] {
|
|
37
|
+
const issues: ChannelStatusIssue[] = [];
|
|
38
|
+
for (const entry of accounts) {
|
|
39
|
+
const account = readZalouserAccountStatus(entry);
|
|
40
|
+
if (!account) continue;
|
|
41
|
+
const accountId = asString(account.accountId) ?? "default";
|
|
42
|
+
const enabled = account.enabled !== false;
|
|
43
|
+
if (!enabled) continue;
|
|
44
|
+
|
|
45
|
+
const configured = account.configured === true;
|
|
46
|
+
const lastError = asString(account.lastError)?.trim();
|
|
47
|
+
|
|
48
|
+
if (!configured) {
|
|
49
|
+
if (isMissingZca(lastError)) {
|
|
50
|
+
issues.push({
|
|
51
|
+
channel: "zalouser",
|
|
52
|
+
accountId,
|
|
53
|
+
kind: "runtime",
|
|
54
|
+
message: "zca CLI not found in PATH.",
|
|
55
|
+
fix: "Install zca-cli and ensure it is on PATH for the Gateway process.",
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
issues.push({
|
|
59
|
+
channel: "zalouser",
|
|
60
|
+
accountId,
|
|
61
|
+
kind: "auth",
|
|
62
|
+
message: "Not authenticated (no zca session).",
|
|
63
|
+
fix: "Run: clawdbot channels login --channel zalouser",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (account.dmPolicy === "open") {
|
|
70
|
+
issues.push({
|
|
71
|
+
channel: "zalouser",
|
|
72
|
+
accountId,
|
|
73
|
+
kind: "config",
|
|
74
|
+
message:
|
|
75
|
+
'Zalo Personal dmPolicy is "open", allowing any user to message the bot without pairing.',
|
|
76
|
+
fix: 'Set channels.zalouser.dmPolicy to "pairing" or "allowlist" to restrict access.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return issues;
|
|
81
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -68,15 +68,14 @@ export type ListenOptions = CommonOptions & {
|
|
|
68
68
|
prefix?: string;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
// Channel plugin config types
|
|
72
|
-
export const DEFAULT_ACCOUNT_ID = "default";
|
|
73
|
-
|
|
74
71
|
export type ZalouserAccountConfig = {
|
|
75
72
|
enabled?: boolean;
|
|
76
73
|
name?: string;
|
|
77
74
|
profile?: string;
|
|
78
75
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
79
76
|
allowFrom?: Array<string | number>;
|
|
77
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
78
|
+
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
|
80
79
|
messagePrefix?: string;
|
|
81
80
|
};
|
|
82
81
|
|
|
@@ -87,18 +86,12 @@ export type ZalouserConfig = {
|
|
|
87
86
|
defaultAccount?: string;
|
|
88
87
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
89
88
|
allowFrom?: Array<string | number>;
|
|
89
|
+
groupPolicy?: "open" | "allowlist" | "disabled";
|
|
90
|
+
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
|
90
91
|
messagePrefix?: string;
|
|
91
92
|
accounts?: Record<string, ZalouserAccountConfig>;
|
|
92
93
|
};
|
|
93
94
|
|
|
94
|
-
export type CoreConfig = {
|
|
95
|
-
channels?: {
|
|
96
|
-
zalouser?: ZalouserConfig;
|
|
97
|
-
[key: string]: unknown;
|
|
98
|
-
};
|
|
99
|
-
[key: string]: unknown;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
95
|
export type ResolvedZalouserAccount = {
|
|
103
96
|
accountId: string;
|
|
104
97
|
name?: string;
|