@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 ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 2026.1.21
4
+
5
+ ### Changes
6
+ - Version alignment with core Clawdbot release numbers.
7
+
8
+ ## 2026.1.20
9
+
10
+ ### Changes
11
+ - Version alignment with core Clawdbot release numbers.
12
+
13
+ ## 2026.1.17-1
14
+
15
+ - Initial version with full channel plugin support
16
+ - QR code login via zca-cli
17
+ - Multi-account support
18
+ - Agent tool for sending messages
19
+ - Group and DM policy support
20
+ - ChannelDock for lightweight shared metadata
21
+ - Zod-based config schema validation
22
+ - Setup adapter for programmatic configuration
23
+ - Dedicated probe and status issues modules
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "zalouser",
3
+ "channels": [
4
+ "zalouser"
5
+ ],
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {}
10
+ }
11
+ }
package/index.ts CHANGED
@@ -1,15 +1,19 @@
1
- import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
1
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
2
3
 
3
- import { zalouserPlugin } from "./src/channel.js";
4
+ import { zalouserDock, zalouserPlugin } from "./src/channel.js";
4
5
  import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
6
+ import { setZalouserRuntime } from "./src/runtime.js";
5
7
 
6
8
  const plugin = {
7
9
  id: "zalouser",
8
10
  name: "Zalo Personal",
9
11
  description: "Zalo personal account messaging via zca-cli",
12
+ configSchema: emptyPluginConfigSchema(),
10
13
  register(api: ClawdbotPluginApi) {
14
+ setZalouserRuntime(api.runtime);
11
15
  // Register channel plugin (for onboarding & gateway)
12
- api.registerChannel(zalouserPlugin);
16
+ api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock });
13
17
 
14
18
  // Register agent tool
15
19
  api.registerTool({
package/package.json CHANGED
@@ -1,14 +1,33 @@
1
1
  {
2
2
  "name": "@clawdbot/zalouser",
3
- "version": "2026.1.16",
3
+ "version": "2026.1.21",
4
4
  "type": "module",
5
5
  "description": "Clawdbot Zalo Personal Account plugin via zca-cli",
6
6
  "dependencies": {
7
+ "clawdbot": "workspace:*",
7
8
  "@sinclair/typebox": "0.34.47"
8
9
  },
9
10
  "clawdbot": {
10
11
  "extensions": [
11
12
  "./index.ts"
12
- ]
13
+ ],
14
+ "channel": {
15
+ "id": "zalouser",
16
+ "label": "Zalo Personal",
17
+ "selectionLabel": "Zalo (Personal Account)",
18
+ "docsPath": "/channels/zalouser",
19
+ "docsLabel": "zalouser",
20
+ "blurb": "Zalo personal account via QR code login.",
21
+ "aliases": [
22
+ "zlu"
23
+ ],
24
+ "order": 85,
25
+ "quickstartAllowFrom": true
26
+ },
27
+ "install": {
28
+ "npmSpec": "@clawdbot/zalouser",
29
+ "localPath": "extensions/zalouser",
30
+ "defaultChoice": "npm"
31
+ }
13
32
  }
14
33
  }
package/src/accounts.ts CHANGED
@@ -1,25 +1,22 @@
1
+ import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
3
+
1
4
  import { runZca, parseJsonOutput } from "./zca.js";
2
- import {
3
- DEFAULT_ACCOUNT_ID,
4
- type CoreConfig,
5
- type ResolvedZalouserAccount,
6
- type ZalouserAccountConfig,
7
- type ZalouserConfig,
8
- } from "./types.js";
5
+ import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
9
6
 
10
- function listConfiguredAccountIds(cfg: CoreConfig): string[] {
7
+ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
11
8
  const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
12
9
  if (!accounts || typeof accounts !== "object") return [];
13
10
  return Object.keys(accounts).filter(Boolean);
14
11
  }
15
12
 
16
- export function listZalouserAccountIds(cfg: CoreConfig): string[] {
13
+ export function listZalouserAccountIds(cfg: ClawdbotConfig): string[] {
17
14
  const ids = listConfiguredAccountIds(cfg);
18
15
  if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
19
16
  return ids.sort((a, b) => a.localeCompare(b));
20
17
  }
21
18
 
22
- export function resolveDefaultZalouserAccountId(cfg: CoreConfig): string {
19
+ export function resolveDefaultZalouserAccountId(cfg: ClawdbotConfig): string {
23
20
  const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
24
21
  if (zalouserConfig?.defaultAccount?.trim()) return zalouserConfig.defaultAccount.trim();
25
22
  const ids = listZalouserAccountIds(cfg);
@@ -27,14 +24,8 @@ export function resolveDefaultZalouserAccountId(cfg: CoreConfig): string {
27
24
  return ids[0] ?? DEFAULT_ACCOUNT_ID;
28
25
  }
29
26
 
30
- export function normalizeAccountId(accountId?: string | null): string {
31
- const trimmed = accountId?.trim();
32
- if (!trimmed) return DEFAULT_ACCOUNT_ID;
33
- return trimmed.toLowerCase();
34
- }
35
-
36
27
  function resolveAccountConfig(
37
- cfg: CoreConfig,
28
+ cfg: ClawdbotConfig,
38
29
  accountId: string,
39
30
  ): ZalouserAccountConfig | undefined {
40
31
  const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
@@ -42,7 +33,10 @@ function resolveAccountConfig(
42
33
  return accounts[accountId] as ZalouserAccountConfig | undefined;
43
34
  }
44
35
 
45
- function mergeZalouserAccountConfig(cfg: CoreConfig, accountId: string): ZalouserAccountConfig {
36
+ function mergeZalouserAccountConfig(
37
+ cfg: ClawdbotConfig,
38
+ accountId: string,
39
+ ): ZalouserAccountConfig {
46
40
  const raw = (cfg.channels?.zalouser ?? {}) as ZalouserConfig;
47
41
  const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
48
42
  const account = resolveAccountConfig(cfg, accountId) ?? {};
@@ -62,7 +56,7 @@ export async function checkZcaAuthenticated(profile: string): Promise<boolean> {
62
56
  }
63
57
 
64
58
  export async function resolveZalouserAccount(params: {
65
- cfg: CoreConfig;
59
+ cfg: ClawdbotConfig;
66
60
  accountId?: string | null;
67
61
  }): Promise<ResolvedZalouserAccount> {
68
62
  const accountId = normalizeAccountId(params.accountId);
@@ -84,7 +78,7 @@ export async function resolveZalouserAccount(params: {
84
78
  }
85
79
 
86
80
  export function resolveZalouserAccountSync(params: {
87
- cfg: CoreConfig;
81
+ cfg: ClawdbotConfig;
88
82
  accountId?: string | null;
89
83
  }): ResolvedZalouserAccount {
90
84
  const accountId = normalizeAccountId(params.accountId);
@@ -104,7 +98,9 @@ export function resolveZalouserAccountSync(params: {
104
98
  };
105
99
  }
106
100
 
107
- export async function listEnabledZalouserAccounts(cfg: CoreConfig): Promise<ResolvedZalouserAccount[]> {
101
+ export async function listEnabledZalouserAccounts(
102
+ cfg: ClawdbotConfig,
103
+ ): Promise<ResolvedZalouserAccount[]> {
108
104
  const ids = listZalouserAccountIds(cfg);
109
105
  const accounts = await Promise.all(
110
106
  ids.map((accountId) => resolveZalouserAccount({ cfg, accountId }))
package/src/channel.ts CHANGED
@@ -1,10 +1,20 @@
1
- import type { ChannelPlugin } from "../../../src/channels/plugins/types.plugin.js";
2
1
  import type {
3
2
  ChannelAccountSnapshot,
4
3
  ChannelDirectoryEntry,
5
- } from "../../../src/channels/plugins/types.core.js";
6
-
7
- import { formatPairingApproveHint } from "../../../src/channels/plugins/helpers.js";
4
+ ChannelDock,
5
+ ChannelPlugin,
6
+ ClawdbotConfig,
7
+ } from "clawdbot/plugin-sdk";
8
+ import {
9
+ applyAccountNameToChannelSection,
10
+ buildChannelConfigSchema,
11
+ DEFAULT_ACCOUNT_ID,
12
+ deleteAccountFromConfigSection,
13
+ formatPairingApproveHint,
14
+ migrateBaseNameToDefaultAccount,
15
+ normalizeAccountId,
16
+ setAccountEnabledInConfigSection,
17
+ } from "clawdbot/plugin-sdk";
8
18
  import {
9
19
  listZalouserAccountIds,
10
20
  resolveDefaultZalouserAccountId,
@@ -16,14 +26,10 @@ import {
16
26
  import { zalouserOnboardingAdapter } from "./onboarding.js";
17
27
  import { sendMessageZalouser } from "./send.js";
18
28
  import { checkZcaInstalled, parseJsonOutput, runZca, runZcaInteractive } from "./zca.js";
19
- import {
20
- DEFAULT_ACCOUNT_ID,
21
- type CoreConfig,
22
- type ZalouserConfig,
23
- type ZcaFriend,
24
- type ZcaGroup,
25
- type ZcaUserInfo,
26
- } from "./types.js";
29
+ import type { ZcaFriend, ZcaGroup, ZcaUserInfo } from "./types.js";
30
+ import { ZalouserConfigSchema } from "./config-schema.js";
31
+ import { collectZalouserStatusIssues } from "./status-issues.js";
32
+ import { probeZalouser } from "./probe.js";
27
33
 
28
34
  const meta = {
29
35
  id: "zalouser",
@@ -38,7 +44,7 @@ const meta = {
38
44
  };
39
45
 
40
46
  function resolveZalouserQrProfile(accountId?: string | null): string {
41
- const normalized = String(accountId ?? "").trim();
47
+ const normalized = normalizeAccountId(accountId);
42
48
  if (!normalized || normalized === DEFAULT_ACCOUNT_ID) {
43
49
  return process.env.ZCA_PROFILE?.trim() || "default";
44
50
  }
@@ -73,64 +79,33 @@ function mapGroup(params: {
73
79
  };
74
80
  }
75
81
 
76
- function deleteAccountFromConfigSection(params: {
77
- cfg: CoreConfig;
78
- accountId: string;
79
- }): CoreConfig {
80
- const { cfg, accountId } = params;
81
- if (accountId === DEFAULT_ACCOUNT_ID) {
82
- const { zalouser: _removed, ...restChannels } = cfg.channels ?? {};
83
- return { ...cfg, channels: restChannels };
84
- }
85
- const accounts = { ...(cfg.channels?.zalouser?.accounts ?? {}) };
86
- delete accounts[accountId];
87
- return {
88
- ...cfg,
89
- channels: {
90
- ...cfg.channels,
91
- zalouser: {
92
- ...cfg.channels?.zalouser,
93
- accounts,
94
- },
95
- },
96
- };
97
- }
98
-
99
- function setAccountEnabledInConfigSection(params: {
100
- cfg: CoreConfig;
101
- accountId: string;
102
- enabled: boolean;
103
- }): CoreConfig {
104
- const { cfg, accountId, enabled } = params;
105
- if (accountId === DEFAULT_ACCOUNT_ID) {
106
- return {
107
- ...cfg,
108
- channels: {
109
- ...cfg.channels,
110
- zalouser: {
111
- ...cfg.channels?.zalouser,
112
- enabled,
113
- },
114
- },
115
- };
116
- }
117
- return {
118
- ...cfg,
119
- channels: {
120
- ...cfg.channels,
121
- zalouser: {
122
- ...cfg.channels?.zalouser,
123
- accounts: {
124
- ...(cfg.channels?.zalouser?.accounts ?? {}),
125
- [accountId]: {
126
- ...(cfg.channels?.zalouser?.accounts?.[accountId] ?? {}),
127
- enabled,
128
- },
129
- },
130
- },
131
- },
132
- };
133
- }
82
+ export const zalouserDock: ChannelDock = {
83
+ id: "zalouser",
84
+ capabilities: {
85
+ chatTypes: ["direct", "group"],
86
+ media: true,
87
+ blockStreaming: true,
88
+ },
89
+ outbound: { textChunkLimit: 2000 },
90
+ config: {
91
+ resolveAllowFrom: ({ cfg, accountId }) =>
92
+ (resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map(
93
+ (entry) => String(entry),
94
+ ),
95
+ formatAllowFrom: ({ allowFrom }) =>
96
+ allowFrom
97
+ .map((entry) => String(entry).trim())
98
+ .filter(Boolean)
99
+ .map((entry) => entry.replace(/^(zalouser|zlu):/i, ""))
100
+ .map((entry) => entry.toLowerCase()),
101
+ },
102
+ groups: {
103
+ resolveRequireMention: () => true,
104
+ },
105
+ threading: {
106
+ resolveReplyToMode: () => "off",
107
+ },
108
+ };
134
109
 
135
110
  export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
136
111
  id: "zalouser",
@@ -146,21 +121,26 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
146
121
  blockStreaming: true,
147
122
  },
148
123
  reload: { configPrefixes: ["channels.zalouser"] },
124
+ configSchema: buildChannelConfigSchema(ZalouserConfigSchema),
149
125
  config: {
150
- listAccountIds: (cfg) => listZalouserAccountIds(cfg as CoreConfig),
126
+ listAccountIds: (cfg) => listZalouserAccountIds(cfg as ClawdbotConfig),
151
127
  resolveAccount: (cfg, accountId) =>
152
- resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }),
153
- defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg as CoreConfig),
128
+ resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId }),
129
+ defaultAccountId: (cfg) => resolveDefaultZalouserAccountId(cfg as ClawdbotConfig),
154
130
  setAccountEnabled: ({ cfg, accountId, enabled }) =>
155
131
  setAccountEnabledInConfigSection({
156
- cfg: cfg as CoreConfig,
132
+ cfg: cfg as ClawdbotConfig,
133
+ sectionKey: "zalouser",
157
134
  accountId,
158
135
  enabled,
136
+ allowTopLevel: true,
159
137
  }),
160
138
  deleteAccount: ({ cfg, accountId }) =>
161
139
  deleteAccountFromConfigSection({
162
- cfg: cfg as CoreConfig,
140
+ cfg: cfg as ClawdbotConfig,
141
+ sectionKey: "zalouser",
163
142
  accountId,
143
+ clearBaseFields: ["profile", "name", "dmPolicy", "allowFrom", "groupPolicy", "groups", "messagePrefix"],
164
144
  }),
165
145
  isConfigured: async (account) => {
166
146
  // Check if zca auth status is OK for this profile
@@ -177,7 +157,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
177
157
  configured: undefined,
178
158
  }),
179
159
  resolveAllowFrom: ({ cfg, accountId }) =>
180
- (resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
160
+ (resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId }).config.allowFrom ?? []).map(
181
161
  (entry) => String(entry),
182
162
  ),
183
163
  formatAllowFrom: ({ allowFrom }) =>
@@ -191,7 +171,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
191
171
  resolveDmPolicy: ({ cfg, accountId, account }) => {
192
172
  const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
193
173
  const useAccountPath = Boolean(
194
- (cfg as CoreConfig).channels?.zalouser?.accounts?.[resolvedAccountId],
174
+ (cfg as ClawdbotConfig).channels?.zalouser?.accounts?.[resolvedAccountId],
195
175
  );
196
176
  const basePath = useAccountPath
197
177
  ? `channels.zalouser.accounts.${resolvedAccountId}.`
@@ -212,6 +192,61 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
212
192
  threading: {
213
193
  resolveReplyToMode: () => "off",
214
194
  },
195
+ setup: {
196
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
197
+ applyAccountName: ({ cfg, accountId, name }) =>
198
+ applyAccountNameToChannelSection({
199
+ cfg: cfg as ClawdbotConfig,
200
+ channelKey: "zalouser",
201
+ accountId,
202
+ name,
203
+ }),
204
+ validateInput: () => null,
205
+ applyAccountConfig: ({ cfg, accountId, input }) => {
206
+ const namedConfig = applyAccountNameToChannelSection({
207
+ cfg: cfg as ClawdbotConfig,
208
+ channelKey: "zalouser",
209
+ accountId,
210
+ name: input.name,
211
+ });
212
+ const next =
213
+ accountId !== DEFAULT_ACCOUNT_ID
214
+ ? migrateBaseNameToDefaultAccount({
215
+ cfg: namedConfig,
216
+ channelKey: "zalouser",
217
+ })
218
+ : namedConfig;
219
+ if (accountId === DEFAULT_ACCOUNT_ID) {
220
+ return {
221
+ ...next,
222
+ channels: {
223
+ ...next.channels,
224
+ zalouser: {
225
+ ...next.channels?.zalouser,
226
+ enabled: true,
227
+ },
228
+ },
229
+ } as ClawdbotConfig;
230
+ }
231
+ return {
232
+ ...next,
233
+ channels: {
234
+ ...next.channels,
235
+ zalouser: {
236
+ ...next.channels?.zalouser,
237
+ enabled: true,
238
+ accounts: {
239
+ ...(next.channels?.zalouser?.accounts ?? {}),
240
+ [accountId]: {
241
+ ...(next.channels?.zalouser?.accounts?.[accountId] ?? {}),
242
+ enabled: true,
243
+ },
244
+ },
245
+ },
246
+ },
247
+ } as ClawdbotConfig;
248
+ },
249
+ },
215
250
  messaging: {
216
251
  normalizeTarget: (raw) => {
217
252
  const trimmed = raw?.trim();
@@ -231,7 +266,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
231
266
  self: async ({ cfg, accountId, runtime }) => {
232
267
  const ok = await checkZcaInstalled();
233
268
  if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
234
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
269
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
235
270
  const result = await runZca(["me", "info", "-j"], { profile: account.profile, timeout: 10000 });
236
271
  if (!result.ok) {
237
272
  runtime.error(result.stderr || "Failed to fetch profile");
@@ -249,7 +284,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
249
284
  listPeers: async ({ cfg, accountId, query, limit }) => {
250
285
  const ok = await checkZcaInstalled();
251
286
  if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
252
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
287
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
253
288
  const args = query?.trim()
254
289
  ? ["friend", "find", query.trim()]
255
290
  : ["friend", "list", "-j"];
@@ -273,7 +308,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
273
308
  listGroups: async ({ cfg, accountId, query, limit }) => {
274
309
  const ok = await checkZcaInstalled();
275
310
  if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
276
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
311
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
277
312
  const result = await runZca(["group", "list", "-j"], { profile: account.profile, timeout: 15000 });
278
313
  if (!result.ok) {
279
314
  throw new Error(result.stderr || "Failed to list groups");
@@ -297,7 +332,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
297
332
  listGroupMembers: async ({ cfg, accountId, groupId, limit }) => {
298
333
  const ok = await checkZcaInstalled();
299
334
  if (!ok) throw new Error("Missing dependency: `zca` not found in PATH");
300
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
335
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
301
336
  const result = await runZca(["group", "members", groupId, "-j"], {
302
337
  profile: account.profile,
303
338
  timeout: 20000,
@@ -324,11 +359,78 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
324
359
  return sliced as ChannelDirectoryEntry[];
325
360
  },
326
361
  },
362
+ resolver: {
363
+ resolveTargets: async ({ cfg, accountId, inputs, kind, runtime }) => {
364
+ const results = [];
365
+ for (const input of inputs) {
366
+ const trimmed = input.trim();
367
+ if (!trimmed) {
368
+ results.push({ input, resolved: false, note: "empty input" });
369
+ continue;
370
+ }
371
+ if (/^\d+$/.test(trimmed)) {
372
+ results.push({ input, resolved: true, id: trimmed });
373
+ continue;
374
+ }
375
+ try {
376
+ const account = resolveZalouserAccountSync({
377
+ cfg: cfg as ClawdbotConfig,
378
+ accountId: accountId ?? DEFAULT_ACCOUNT_ID,
379
+ });
380
+ const args =
381
+ kind === "user"
382
+ ? trimmed
383
+ ? ["friend", "find", trimmed]
384
+ : ["friend", "list", "-j"]
385
+ : ["group", "list", "-j"];
386
+ const result = await runZca(args, { profile: account.profile, timeout: 15000 });
387
+ if (!result.ok) throw new Error(result.stderr || "zca lookup failed");
388
+ if (kind === "user") {
389
+ const parsed = parseJsonOutput<ZcaFriend[]>(result.stdout) ?? [];
390
+ const matches = Array.isArray(parsed)
391
+ ? parsed.map((f) => ({
392
+ id: String(f.userId),
393
+ name: f.displayName ?? undefined,
394
+ }))
395
+ : [];
396
+ const best = matches[0];
397
+ results.push({
398
+ input,
399
+ resolved: Boolean(best?.id),
400
+ id: best?.id,
401
+ name: best?.name,
402
+ note: matches.length > 1 ? "multiple matches; chose first" : undefined,
403
+ });
404
+ } else {
405
+ const parsed = parseJsonOutput<ZcaGroup[]>(result.stdout) ?? [];
406
+ const matches = Array.isArray(parsed)
407
+ ? parsed.map((g) => ({
408
+ id: String(g.groupId),
409
+ name: g.name ?? undefined,
410
+ }))
411
+ : [];
412
+ const best = matches.find((g) => g.name?.toLowerCase() === trimmed.toLowerCase()) ?? matches[0];
413
+ results.push({
414
+ input,
415
+ resolved: Boolean(best?.id),
416
+ id: best?.id,
417
+ name: best?.name,
418
+ note: matches.length > 1 ? "multiple matches; chose first" : undefined,
419
+ });
420
+ }
421
+ } catch (err) {
422
+ runtime.error?.(`zalouser resolve failed: ${String(err)}`);
423
+ results.push({ input, resolved: false, note: "lookup failed" });
424
+ }
425
+ }
426
+ return results;
427
+ },
428
+ },
327
429
  pairing: {
328
430
  idLabel: "zalouserUserId",
329
431
  normalizeAllowEntry: (entry) => entry.replace(/^(zalouser|zlu):/i, ""),
330
432
  notifyApproval: async ({ cfg, id }) => {
331
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig });
433
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig });
332
434
  const authenticated = await checkZcaAuthenticated(account.profile);
333
435
  if (!authenticated) throw new Error("Zalouser not authenticated");
334
436
  await sendMessageZalouser(id, "Your pairing request has been approved.", {
@@ -339,7 +441,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
339
441
  auth: {
340
442
  login: async ({ cfg, accountId, runtime }) => {
341
443
  const account = resolveZalouserAccountSync({
342
- cfg: cfg as CoreConfig,
444
+ cfg: cfg as ClawdbotConfig,
343
445
  accountId: accountId ?? DEFAULT_ACCOUNT_ID,
344
446
  });
345
447
  const ok = await checkZcaInstalled();
@@ -382,7 +484,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
382
484
  },
383
485
  textChunkLimit: 2000,
384
486
  sendText: async ({ to, text, accountId, cfg }) => {
385
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
487
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
386
488
  const result = await sendMessageZalouser(to, text, { profile: account.profile });
387
489
  return {
388
490
  channel: "zalouser",
@@ -392,7 +494,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
392
494
  };
393
495
  },
394
496
  sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
395
- const account = resolveZalouserAccountSync({ cfg: cfg as CoreConfig, accountId });
497
+ const account = resolveZalouserAccountSync({ cfg: cfg as ClawdbotConfig, accountId });
396
498
  const result = await sendMessageZalouser(to, text, {
397
499
  profile: account.profile,
398
500
  mediaUrl,
@@ -413,6 +515,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
413
515
  lastStopAt: null,
414
516
  lastError: null,
415
517
  },
518
+ collectStatusIssues: collectZalouserStatusIssues,
416
519
  buildChannelSummary: ({ snapshot }) => ({
417
520
  configured: snapshot.configured ?? false,
418
521
  running: snapshot.running ?? false,
@@ -422,22 +525,12 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
422
525
  probe: snapshot.probe,
423
526
  lastProbeAt: snapshot.lastProbeAt ?? null,
424
527
  }),
425
- probeAccount: async ({ account, timeoutMs }) => {
426
- const result = await runZca(["me", "info", "-j"], {
427
- profile: account.profile,
428
- timeout: timeoutMs,
429
- });
430
- if (!result.ok) {
431
- return { ok: false, error: result.stderr };
432
- }
433
- try {
434
- return { ok: true, user: JSON.parse(result.stdout) };
435
- } catch {
436
- return { ok: false, error: "Failed to parse user info" };
437
- }
438
- },
528
+ probeAccount: async ({ account, timeoutMs }) =>
529
+ probeZalouser(account.profile, timeoutMs),
439
530
  buildAccountSnapshot: async ({ account, runtime }) => {
440
- const configured = await checkZcaAuthenticated(account.profile);
531
+ const zcaInstalled = await checkZcaInstalled();
532
+ const configured = zcaInstalled ? await checkZcaAuthenticated(account.profile) : false;
533
+ const configError = zcaInstalled ? "not authenticated" : "zca CLI not found in PATH";
441
534
  return {
442
535
  accountId: account.accountId,
443
536
  name: account.name,
@@ -446,7 +539,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
446
539
  running: runtime?.running ?? false,
447
540
  lastStartAt: runtime?.lastStartAt ?? null,
448
541
  lastStopAt: runtime?.lastStopAt ?? null,
449
- lastError: configured ? (runtime?.lastError ?? null) : "not configured",
542
+ lastError: configured ? (runtime?.lastError ?? null) : runtime?.lastError ?? configError,
450
543
  lastInboundAt: runtime?.lastInboundAt ?? null,
451
544
  lastOutboundAt: runtime?.lastOutboundAt ?? null,
452
545
  dmPolicy: account.config.dmPolicy ?? "pairing",
@@ -471,7 +564,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
471
564
  const { monitorZalouserProvider } = await import("./monitor.js");
472
565
  return monitorZalouserProvider({
473
566
  account,
474
- config: ctx.cfg as CoreConfig,
567
+ config: ctx.cfg as ClawdbotConfig,
475
568
  runtime: ctx.runtime,
476
569
  abortSignal: ctx.abortSignal,
477
570
  statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+
3
+ const allowFromEntry = z.union([z.string(), z.number()]);
4
+
5
+ const groupConfigSchema = z.object({
6
+ allow: z.boolean().optional(),
7
+ enabled: z.boolean().optional(),
8
+ });
9
+
10
+ const zalouserAccountSchema = z.object({
11
+ name: z.string().optional(),
12
+ enabled: z.boolean().optional(),
13
+ profile: z.string().optional(),
14
+ dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
15
+ allowFrom: z.array(allowFromEntry).optional(),
16
+ groupPolicy: z.enum(["disabled", "allowlist", "open"]).optional(),
17
+ groups: z.object({}).catchall(groupConfigSchema).optional(),
18
+ messagePrefix: z.string().optional(),
19
+ });
20
+
21
+ export const ZalouserConfigSchema = zalouserAccountSchema.extend({
22
+ accounts: z.object({}).catchall(zalouserAccountSchema).optional(),
23
+ defaultAccount: z.string().optional(),
24
+ });