@openclaw/zalouser 2026.3.2 → 2026.3.7

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.7
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.3.3
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
3
15
  ## 2026.3.2
4
16
 
5
17
  ### Changes
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/zalouser";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalouser";
3
3
  import { zalouserDock, zalouserPlugin } from "./src/channel.js";
4
4
  import { setZalouserRuntime } from "./src/runtime.js";
5
5
  import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/zalouser",
3
- "version": "2026.3.2",
3
+ "version": "2026.3.7",
4
4
  "description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@sinclair/typebox": "0.34.48",
8
- "zca-js": "2.1.1"
8
+ "zca-js": "2.1.1",
9
+ "zod": "^4.3.6"
9
10
  },
10
11
  "openclaw": {
11
12
  "extensions": [
@@ -1,5 +1,5 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
1
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/zalouser";
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import {
5
5
  getZcaUserInfo,
package/src/accounts.ts CHANGED
@@ -1,43 +1,13 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
- import {
3
- DEFAULT_ACCOUNT_ID,
4
- normalizeAccountId,
5
- normalizeOptionalAccountId,
6
- } from "openclaw/plugin-sdk/account-id";
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/zalouser";
7
3
  import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
8
4
  import { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js";
9
5
 
10
- function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
11
- const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
12
- if (!accounts || typeof accounts !== "object") {
13
- return [];
14
- }
15
- return Object.keys(accounts).filter(Boolean);
16
- }
17
-
18
- export function listZalouserAccountIds(cfg: OpenClawConfig): string[] {
19
- const ids = listConfiguredAccountIds(cfg);
20
- if (ids.length === 0) {
21
- return [DEFAULT_ACCOUNT_ID];
22
- }
23
- return ids.toSorted((a, b) => a.localeCompare(b));
24
- }
25
-
26
- export function resolveDefaultZalouserAccountId(cfg: OpenClawConfig): string {
27
- const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
28
- const preferred = normalizeOptionalAccountId(zalouserConfig?.defaultAccount);
29
- if (
30
- preferred &&
31
- listZalouserAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
32
- ) {
33
- return preferred;
34
- }
35
- const ids = listZalouserAccountIds(cfg);
36
- if (ids.includes(DEFAULT_ACCOUNT_ID)) {
37
- return DEFAULT_ACCOUNT_ID;
38
- }
39
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
40
- }
6
+ const {
7
+ listAccountIds: listZalouserAccountIds,
8
+ resolveDefaultAccountId: resolveDefaultZalouserAccountId,
9
+ } = createAccountListHelpers("zalouser");
10
+ export { listZalouserAccountIds, resolveDefaultZalouserAccountId };
41
11
 
42
12
  function resolveAccountConfig(
43
13
  cfg: OpenClawConfig,
@@ -1,4 +1,4 @@
1
- import type { ReplyPayload } from "openclaw/plugin-sdk";
1
+ import type { ReplyPayload } from "openclaw/plugin-sdk/zalouser";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { zalouserPlugin } from "./channel.js";
4
4
 
package/src/channel.ts CHANGED
@@ -1,5 +1,7 @@
1
- import fsp from "node:fs/promises";
2
- import path from "node:path";
1
+ import {
2
+ buildAccountScopedDmSecurityPolicy,
3
+ mapAllowFromEntries,
4
+ } from "openclaw/plugin-sdk/compat";
3
5
  import type {
4
6
  ChannelAccountSnapshot,
5
7
  ChannelDirectoryEntry,
@@ -9,21 +11,23 @@ import type {
9
11
  ChannelPlugin,
10
12
  OpenClawConfig,
11
13
  GroupToolPolicyConfig,
12
- } from "openclaw/plugin-sdk";
14
+ } from "openclaw/plugin-sdk/zalouser";
13
15
  import {
14
16
  applyAccountNameToChannelSection,
17
+ applySetupAccountConfigPatch,
18
+ buildChannelSendResult,
19
+ buildBaseAccountStatusSnapshot,
15
20
  buildChannelConfigSchema,
16
21
  DEFAULT_ACCOUNT_ID,
17
22
  chunkTextForOutbound,
18
23
  deleteAccountFromConfigSection,
19
24
  formatAllowFromLowercase,
20
- formatPairingApproveHint,
25
+ isNumericTargetId,
21
26
  migrateBaseNameToDefaultAccount,
22
27
  normalizeAccountId,
23
- resolvePreferredOpenClawTmpDir,
24
- resolveChannelAccountConfigBasePath,
28
+ sendPayloadWithChunkedTextAndMedia,
25
29
  setAccountEnabledInConfigSection,
26
- } from "openclaw/plugin-sdk";
30
+ } from "openclaw/plugin-sdk/zalouser";
27
31
  import {
28
32
  listZalouserAccountIds,
29
33
  resolveDefaultZalouserAccountId,
@@ -37,6 +41,7 @@ import { buildZalouserGroupCandidates, findZalouserGroupEntry } from "./group-po
37
41
  import { resolveZalouserReactionMessageIds } from "./message-sid.js";
38
42
  import { zalouserOnboardingAdapter } from "./onboarding.js";
39
43
  import { probeZalouser } from "./probe.js";
44
+ import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
40
45
  import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
41
46
  import { collectZalouserStatusIssues } from "./status-issues.js";
42
47
  import {
@@ -69,25 +74,6 @@ function resolveZalouserQrProfile(accountId?: string | null): string {
69
74
  return normalized;
70
75
  }
71
76
 
72
- async function writeQrDataUrlToTempFile(
73
- qrDataUrl: string,
74
- profile: string,
75
- ): Promise<string | null> {
76
- const trimmed = qrDataUrl.trim();
77
- const match = trimmed.match(/^data:image\/png;base64,(.+)$/i);
78
- const base64 = (match?.[1] ?? "").trim();
79
- if (!base64) {
80
- return null;
81
- }
82
- const safeProfile = profile.replace(/[^a-zA-Z0-9_-]+/g, "-") || "default";
83
- const filePath = path.join(
84
- resolvePreferredOpenClawTmpDir(),
85
- `openclaw-zalouser-qr-${safeProfile}.png`,
86
- );
87
- await fsp.writeFile(filePath, Buffer.from(base64, "base64"));
88
- return filePath;
89
- }
90
-
91
77
  function mapUser(params: {
92
78
  id: string;
93
79
  name?: string | null;
@@ -116,15 +102,13 @@ function mapGroup(params: {
116
102
  };
117
103
  }
118
104
 
119
- function resolveZalouserGroupToolPolicy(
120
- params: ChannelGroupContext,
121
- ): GroupToolPolicyConfig | undefined {
105
+ function resolveZalouserGroupPolicyEntry(params: ChannelGroupContext) {
122
106
  const account = resolveZalouserAccountSync({
123
107
  cfg: params.cfg,
124
108
  accountId: params.accountId ?? undefined,
125
109
  });
126
110
  const groups = account.config.groups ?? {};
127
- const entry = findZalouserGroupEntry(
111
+ return findZalouserGroupEntry(
128
112
  groups,
129
113
  buildZalouserGroupCandidates({
130
114
  groupId: params.groupId,
@@ -132,23 +116,16 @@ function resolveZalouserGroupToolPolicy(
132
116
  includeWildcard: true,
133
117
  }),
134
118
  );
135
- return entry?.tools;
119
+ }
120
+
121
+ function resolveZalouserGroupToolPolicy(
122
+ params: ChannelGroupContext,
123
+ ): GroupToolPolicyConfig | undefined {
124
+ return resolveZalouserGroupPolicyEntry(params)?.tools;
136
125
  }
137
126
 
138
127
  function resolveZalouserRequireMention(params: ChannelGroupContext): boolean {
139
- const account = resolveZalouserAccountSync({
140
- cfg: params.cfg,
141
- accountId: params.accountId ?? undefined,
142
- });
143
- const groups = account.config.groups ?? {};
144
- const entry = findZalouserGroupEntry(
145
- groups,
146
- buildZalouserGroupCandidates({
147
- groupId: params.groupId,
148
- groupChannel: params.groupChannel,
149
- includeWildcard: true,
150
- }),
151
- );
128
+ const entry = resolveZalouserGroupPolicyEntry(params);
152
129
  if (typeof entry?.requireMention === "boolean") {
153
130
  return entry.requireMention;
154
131
  }
@@ -234,9 +211,7 @@ export const zalouserDock: ChannelDock = {
234
211
  outbound: { textChunkLimit: 2000 },
235
212
  config: {
236
213
  resolveAllowFrom: ({ cfg, accountId }) =>
237
- (resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) =>
238
- String(entry),
239
- ),
214
+ mapAllowFromEntries(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom),
240
215
  formatAllowFrom: ({ allowFrom }) =>
241
216
  formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
242
217
  },
@@ -299,28 +274,22 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
299
274
  configured: undefined,
300
275
  }),
301
276
  resolveAllowFrom: ({ cfg, accountId }) =>
302
- (resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) =>
303
- String(entry),
304
- ),
277
+ mapAllowFromEntries(resolveZalouserAccountSync({ cfg: cfg, accountId }).config.allowFrom),
305
278
  formatAllowFrom: ({ allowFrom }) =>
306
279
  formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(zalouser|zlu):/i }),
307
280
  },
308
281
  security: {
309
282
  resolveDmPolicy: ({ cfg, accountId, account }) => {
310
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
311
- const basePath = resolveChannelAccountConfigBasePath({
283
+ return buildAccountScopedDmSecurityPolicy({
312
284
  cfg,
313
285
  channelKey: "zalouser",
314
- accountId: resolvedAccountId,
315
- });
316
- return {
317
- policy: account.config.dmPolicy ?? "pairing",
286
+ accountId,
287
+ fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
288
+ policy: account.config.dmPolicy,
318
289
  allowFrom: account.config.allowFrom ?? [],
319
- policyPath: `${basePath}dmPolicy`,
320
- allowFromPath: basePath,
321
- approveHint: formatPairingApproveHint("zalouser"),
290
+ policyPathSuffix: "dmPolicy",
322
291
  normalizeEntry: (raw) => raw.replace(/^(zalouser|zlu):/i, ""),
323
- };
292
+ });
324
293
  },
325
294
  },
326
295
  groups: {
@@ -355,35 +324,12 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
355
324
  channelKey: "zalouser",
356
325
  })
357
326
  : namedConfig;
358
- if (accountId === DEFAULT_ACCOUNT_ID) {
359
- return {
360
- ...next,
361
- channels: {
362
- ...next.channels,
363
- zalouser: {
364
- ...next.channels?.zalouser,
365
- enabled: true,
366
- },
367
- },
368
- } as OpenClawConfig;
369
- }
370
- return {
371
- ...next,
372
- channels: {
373
- ...next.channels,
374
- zalouser: {
375
- ...next.channels?.zalouser,
376
- enabled: true,
377
- accounts: {
378
- ...next.channels?.zalouser?.accounts,
379
- [accountId]: {
380
- ...next.channels?.zalouser?.accounts?.[accountId],
381
- enabled: true,
382
- },
383
- },
384
- },
385
- },
386
- } as OpenClawConfig;
327
+ return applySetupAccountConfigPatch({
328
+ cfg: next,
329
+ channelKey: "zalouser",
330
+ accountId,
331
+ patch: {},
332
+ });
387
333
  },
388
334
  },
389
335
  messaging: {
@@ -395,13 +341,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
395
341
  return trimmed.replace(/^(zalouser|zlu):/i, "");
396
342
  },
397
343
  targetResolver: {
398
- looksLikeId: (raw) => {
399
- const trimmed = raw.trim();
400
- if (!trimmed) {
401
- return false;
402
- }
403
- return /^\d{3,}$/.test(trimmed);
404
- },
344
+ looksLikeId: isNumericTargetId,
405
345
  hint: "<threadId>",
406
346
  },
407
347
  },
@@ -560,49 +500,19 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
560
500
  chunker: chunkTextForOutbound,
561
501
  chunkerMode: "text",
562
502
  textChunkLimit: 2000,
563
- sendPayload: async (ctx) => {
564
- const text = ctx.payload.text ?? "";
565
- const urls = ctx.payload.mediaUrls?.length
566
- ? ctx.payload.mediaUrls
567
- : ctx.payload.mediaUrl
568
- ? [ctx.payload.mediaUrl]
569
- : [];
570
- if (!text && urls.length === 0) {
571
- return { channel: "zalouser", messageId: "" };
572
- }
573
- if (urls.length > 0) {
574
- let lastResult = await zalouserPlugin.outbound!.sendMedia!({
575
- ...ctx,
576
- text,
577
- mediaUrl: urls[0],
578
- });
579
- for (let i = 1; i < urls.length; i++) {
580
- lastResult = await zalouserPlugin.outbound!.sendMedia!({
581
- ...ctx,
582
- text: "",
583
- mediaUrl: urls[i],
584
- });
585
- }
586
- return lastResult;
587
- }
588
- const outbound = zalouserPlugin.outbound!;
589
- const limit = outbound.textChunkLimit;
590
- const chunks = limit && outbound.chunker ? outbound.chunker(text, limit) : [text];
591
- let lastResult: Awaited<ReturnType<NonNullable<typeof outbound.sendText>>>;
592
- for (const chunk of chunks) {
593
- lastResult = await outbound.sendText!({ ...ctx, text: chunk });
594
- }
595
- return lastResult!;
596
- },
503
+ sendPayload: async (ctx) =>
504
+ await sendPayloadWithChunkedTextAndMedia({
505
+ ctx,
506
+ textChunkLimit: zalouserPlugin.outbound!.textChunkLimit,
507
+ chunker: zalouserPlugin.outbound!.chunker,
508
+ sendText: (nextCtx) => zalouserPlugin.outbound!.sendText!(nextCtx),
509
+ sendMedia: (nextCtx) => zalouserPlugin.outbound!.sendMedia!(nextCtx),
510
+ emptyResult: { channel: "zalouser", messageId: "" },
511
+ }),
597
512
  sendText: async ({ to, text, accountId, cfg }) => {
598
513
  const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
599
514
  const result = await sendMessageZalouser(to, text, { profile: account.profile });
600
- return {
601
- channel: "zalouser",
602
- ok: result.ok,
603
- messageId: result.messageId ?? "",
604
- error: result.error ? new Error(result.error) : undefined,
605
- };
515
+ return buildChannelSendResult("zalouser", result);
606
516
  },
607
517
  sendMedia: async ({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots }) => {
608
518
  const account = resolveZalouserAccountSync({ cfg: cfg, accountId });
@@ -611,12 +521,7 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
611
521
  mediaUrl,
612
522
  mediaLocalRoots,
613
523
  });
614
- return {
615
- channel: "zalouser",
616
- ok: result.ok,
617
- messageId: result.messageId ?? "",
618
- error: result.error ? new Error(result.error) : undefined,
619
- };
524
+ return buildChannelSendResult("zalouser", result);
620
525
  },
621
526
  },
622
527
  status: {
@@ -641,17 +546,19 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
641
546
  buildAccountSnapshot: async ({ account, runtime }) => {
642
547
  const configured = await checkZcaAuthenticated(account.profile);
643
548
  const configError = "not authenticated";
549
+ const base = buildBaseAccountStatusSnapshot({
550
+ account: {
551
+ accountId: account.accountId,
552
+ name: account.name,
553
+ enabled: account.enabled,
554
+ configured,
555
+ },
556
+ runtime: configured
557
+ ? runtime
558
+ : { ...runtime, lastError: runtime?.lastError ?? configError },
559
+ });
644
560
  return {
645
- accountId: account.accountId,
646
- name: account.name,
647
- enabled: account.enabled,
648
- configured,
649
- running: runtime?.running ?? false,
650
- lastStartAt: runtime?.lastStartAt ?? null,
651
- lastStopAt: runtime?.lastStopAt ?? null,
652
- lastError: configured ? (runtime?.lastError ?? null) : (runtime?.lastError ?? configError),
653
- lastInboundAt: runtime?.lastInboundAt ?? null,
654
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
561
+ ...base,
655
562
  dmPolicy: account.config.dmPolicy ?? "pairing",
656
563
  };
657
564
  },
@@ -1,4 +1,4 @@
1
- import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
1
+ import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/zalouser";
2
2
  import { z } from "zod";
3
3
 
4
4
  const allowFromEntry = z.union([z.string(), z.number()]);
@@ -1,21 +1,11 @@
1
- import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/zalouser";
2
2
  import { describe, expect, it, vi } from "vitest";
3
+ import "./monitor.send-mocks.js";
3
4
  import { __testing } from "./monitor.js";
5
+ import { sendMessageZalouserMock } from "./monitor.send-mocks.js";
4
6
  import { setZalouserRuntime } from "./runtime.js";
5
7
  import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
6
8
 
7
- const sendMessageZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
8
- const sendTypingZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
9
- const sendDeliveredZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
10
- const sendSeenZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
11
-
12
- vi.mock("./send.js", () => ({
13
- sendMessageZalouser: sendMessageZalouserMock,
14
- sendTypingZalouser: sendTypingZalouserMock,
15
- sendDeliveredZalouser: sendDeliveredZalouserMock,
16
- sendSeenZalouser: sendSeenZalouserMock,
17
- }));
18
-
19
9
  describe("zalouser monitor pairing account scoping", () => {
20
10
  it("scopes DM pairing-store reads and pairing requests to accountId", async () => {
21
11
  const readAllowFromStore = vi.fn(
@@ -1,21 +1,16 @@
1
- import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/zalouser";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import "./monitor.send-mocks.js";
3
4
  import { __testing } from "./monitor.js";
5
+ import {
6
+ sendDeliveredZalouserMock,
7
+ sendMessageZalouserMock,
8
+ sendSeenZalouserMock,
9
+ sendTypingZalouserMock,
10
+ } from "./monitor.send-mocks.js";
4
11
  import { setZalouserRuntime } from "./runtime.js";
5
12
  import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
6
13
 
7
- const sendMessageZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
8
- const sendTypingZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
9
- const sendDeliveredZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
10
- const sendSeenZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
11
-
12
- vi.mock("./send.js", () => ({
13
- sendMessageZalouser: sendMessageZalouserMock,
14
- sendTypingZalouser: sendTypingZalouserMock,
15
- sendDeliveredZalouser: sendDeliveredZalouserMock,
16
- sendSeenZalouser: sendSeenZalouserMock,
17
- }));
18
-
19
14
  function createAccount(): ResolvedZalouserAccount {
20
15
  return {
21
16
  accountId: "default",
@@ -0,0 +1,20 @@
1
+ import { vi } from "vitest";
2
+
3
+ const sendMocks = vi.hoisted(() => ({
4
+ sendMessageZalouserMock: vi.fn(async () => {}),
5
+ sendTypingZalouserMock: vi.fn(async () => {}),
6
+ sendDeliveredZalouserMock: vi.fn(async () => {}),
7
+ sendSeenZalouserMock: vi.fn(async () => {}),
8
+ }));
9
+
10
+ export const sendMessageZalouserMock = sendMocks.sendMessageZalouserMock;
11
+ export const sendTypingZalouserMock = sendMocks.sendTypingZalouserMock;
12
+ export const sendDeliveredZalouserMock = sendMocks.sendDeliveredZalouserMock;
13
+ export const sendSeenZalouserMock = sendMocks.sendSeenZalouserMock;
14
+
15
+ vi.mock("./send.js", () => ({
16
+ sendMessageZalouser: sendMessageZalouserMock,
17
+ sendTypingZalouser: sendTypingZalouserMock,
18
+ sendDeliveredZalouser: sendDeliveredZalouserMock,
19
+ sendSeenZalouser: sendSeenZalouserMock,
20
+ }));
package/src/monitor.ts CHANGED
@@ -3,11 +3,13 @@ import type {
3
3
  OpenClawConfig,
4
4
  OutboundReplyPayload,
5
5
  RuntimeEnv,
6
- } from "openclaw/plugin-sdk";
6
+ } from "openclaw/plugin-sdk/zalouser";
7
7
  import {
8
8
  createTypingCallbacks,
9
9
  createScopedPairingAccess,
10
10
  createReplyPrefixOptions,
11
+ evaluateGroupRouteAccessForPolicy,
12
+ issuePairingChallenge,
11
13
  resolveOutboundMediaUrls,
12
14
  mergeAllowlist,
13
15
  resolveMentionGatingWithBypass,
@@ -17,7 +19,7 @@ import {
17
19
  sendMediaWithLeadingCaption,
18
20
  summarizeMapping,
19
21
  warnMissingProviderGroupPolicyFallbackOnce,
20
- } from "openclaw/plugin-sdk";
22
+ } from "openclaw/plugin-sdk/zalouser";
21
23
  import {
22
24
  buildZalouserGroupCandidates,
23
25
  findZalouserGroupEntry,
@@ -93,28 +95,6 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
93
95
  });
94
96
  }
95
97
 
96
- function isGroupAllowed(params: {
97
- groupId: string;
98
- groupName?: string | null;
99
- groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
100
- }): boolean {
101
- const groups = params.groups ?? {};
102
- const keys = Object.keys(groups);
103
- if (keys.length === 0) {
104
- return false;
105
- }
106
- const entry = findZalouserGroupEntry(
107
- groups,
108
- buildZalouserGroupCandidates({
109
- groupId: params.groupId,
110
- groupName: params.groupName,
111
- includeGroupIdAlias: true,
112
- includeWildcard: true,
113
- }),
114
- );
115
- return isZalouserGroupEntryAllowed(entry);
116
- }
117
-
118
98
  function resolveGroupRequireMention(params: {
119
99
  groupId: string;
120
100
  groupName?: string | null;
@@ -222,16 +202,36 @@ async function processMessage(
222
202
 
223
203
  const groups = account.config.groups ?? {};
224
204
  if (isGroup) {
225
- if (groupPolicy === "disabled") {
226
- logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
227
- return;
228
- }
229
- if (groupPolicy === "allowlist") {
230
- const allowed = isGroupAllowed({ groupId: chatId, groupName, groups });
231
- if (!allowed) {
205
+ const groupEntry = findZalouserGroupEntry(
206
+ groups,
207
+ buildZalouserGroupCandidates({
208
+ groupId: chatId,
209
+ groupName,
210
+ includeGroupIdAlias: true,
211
+ includeWildcard: true,
212
+ }),
213
+ );
214
+ const routeAccess = evaluateGroupRouteAccessForPolicy({
215
+ groupPolicy,
216
+ routeAllowlistConfigured: Object.keys(groups).length > 0,
217
+ routeMatched: Boolean(groupEntry),
218
+ routeEnabled: isZalouserGroupEntryAllowed(groupEntry),
219
+ });
220
+ if (!routeAccess.allowed) {
221
+ if (routeAccess.reason === "disabled") {
222
+ logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
223
+ } else if (routeAccess.reason === "empty_allowlist") {
224
+ logVerbose(
225
+ core,
226
+ runtime,
227
+ `zalouser: drop group ${chatId} (groupPolicy=allowlist, no allowlist)`,
228
+ );
229
+ } else if (routeAccess.reason === "route_not_allowlisted") {
232
230
  logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
233
- return;
231
+ } else if (routeAccess.reason === "route_disabled") {
232
+ logVerbose(core, runtime, `zalouser: drop group ${chatId} (group disabled)`);
234
233
  }
234
+ return;
235
235
  }
236
236
  }
237
237
 
@@ -262,32 +262,27 @@ async function processMessage(
262
262
  const allowed = senderAllowedForCommands;
263
263
  if (!allowed) {
264
264
  if (dmPolicy === "pairing") {
265
- const { code, created } = await pairing.upsertPairingRequest({
266
- id: senderId,
265
+ await issuePairingChallenge({
266
+ channel: "zalouser",
267
+ senderId,
268
+ senderIdLine: `Your Zalo user id: ${senderId}`,
267
269
  meta: { name: senderName || undefined },
268
- });
269
-
270
- if (created) {
271
- logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
272
- try {
273
- await sendMessageZalouser(
274
- chatId,
275
- core.channel.pairing.buildPairingReply({
276
- channel: "zalouser",
277
- idLine: `Your Zalo user id: ${senderId}`,
278
- code,
279
- }),
280
- { profile: account.profile },
281
- );
270
+ upsertPairingRequest: pairing.upsertPairingRequest,
271
+ onCreated: () => {
272
+ logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
273
+ },
274
+ sendPairingReply: async (text) => {
275
+ await sendMessageZalouser(chatId, text, { profile: account.profile });
282
276
  statusSink?.({ lastOutboundAt: Date.now() });
283
- } catch (err) {
277
+ },
278
+ onReplyError: (err) => {
284
279
  logVerbose(
285
280
  core,
286
281
  runtime,
287
282
  `zalouser pairing reply failed for ${senderId}: ${String(err)}`,
288
283
  );
289
- }
290
- }
284
+ },
285
+ });
291
286
  } else {
292
287
  logVerbose(
293
288
  core,
package/src/onboarding.ts CHANGED
@@ -1,27 +1,25 @@
1
- import fsp from "node:fs/promises";
2
- import path from "node:path";
3
1
  import type {
4
2
  ChannelOnboardingAdapter,
5
3
  ChannelOnboardingDmPolicy,
6
4
  OpenClawConfig,
7
5
  WizardPrompter,
8
- } from "openclaw/plugin-sdk";
6
+ } from "openclaw/plugin-sdk/zalouser";
9
7
  import {
10
- addWildcardAllowFrom,
11
8
  DEFAULT_ACCOUNT_ID,
12
9
  formatResolvedUnresolvedNote,
13
10
  mergeAllowFromEntries,
14
11
  normalizeAccountId,
15
- promptAccountId,
16
12
  promptChannelAccessConfig,
17
- resolvePreferredOpenClawTmpDir,
18
- } from "openclaw/plugin-sdk";
13
+ resolveAccountIdForConfigure,
14
+ setTopLevelChannelDmPolicyWithAllowFrom,
15
+ } from "openclaw/plugin-sdk/zalouser";
19
16
  import {
20
17
  listZalouserAccountIds,
21
18
  resolveDefaultZalouserAccountId,
22
19
  resolveZalouserAccountSync,
23
20
  checkZcaAuthenticated,
24
21
  } from "./accounts.js";
22
+ import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
25
23
  import {
26
24
  logoutZaloProfile,
27
25
  resolveZaloAllowFromEntries,
@@ -75,19 +73,11 @@ function setZalouserDmPolicy(
75
73
  cfg: OpenClawConfig,
76
74
  dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
77
75
  ): OpenClawConfig {
78
- const allowFrom =
79
- dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalouser?.allowFrom) : undefined;
80
- return {
81
- ...cfg,
82
- channels: {
83
- ...cfg.channels,
84
- zalouser: {
85
- ...cfg.channels?.zalouser,
86
- dmPolicy,
87
- ...(allowFrom ? { allowFrom } : {}),
88
- },
89
- },
90
- } as OpenClawConfig;
76
+ return setTopLevelChannelDmPolicyWithAllowFrom({
77
+ cfg,
78
+ channel: "zalouser",
79
+ dmPolicy,
80
+ }) as OpenClawConfig;
91
81
  }
92
82
 
93
83
  async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
@@ -103,25 +93,6 @@ async function noteZalouserHelp(prompter: WizardPrompter): Promise<void> {
103
93
  );
104
94
  }
105
95
 
106
- async function writeQrDataUrlToTempFile(
107
- qrDataUrl: string,
108
- profile: string,
109
- ): Promise<string | null> {
110
- const trimmed = qrDataUrl.trim();
111
- const match = trimmed.match(/^data:image\/png;base64,(.+)$/i);
112
- const base64 = (match?.[1] ?? "").trim();
113
- if (!base64) {
114
- return null;
115
- }
116
- const safeProfile = profile.replace(/[^a-zA-Z0-9_-]+/g, "-") || "default";
117
- const filePath = path.join(
118
- resolvePreferredOpenClawTmpDir(),
119
- `openclaw-zalouser-qr-${safeProfile}.png`,
120
- );
121
- await fsp.writeFile(filePath, Buffer.from(base64, "base64"));
122
- return filePath;
123
- }
124
-
125
96
  async function promptZalouserAllowFrom(params: {
126
97
  cfg: OpenClawConfig;
127
98
  prompter: WizardPrompter;
@@ -247,20 +218,16 @@ export const zalouserOnboardingAdapter: ChannelOnboardingAdapter = {
247
218
  shouldPromptAccountIds,
248
219
  forceAllowFrom,
249
220
  }) => {
250
- const zalouserOverride = accountOverrides.zalouser?.trim();
251
221
  const defaultAccountId = resolveDefaultZalouserAccountId(cfg);
252
- let accountId = zalouserOverride ? normalizeAccountId(zalouserOverride) : defaultAccountId;
253
-
254
- if (shouldPromptAccountIds && !zalouserOverride) {
255
- accountId = await promptAccountId({
256
- cfg,
257
- prompter,
258
- label: "Zalo Personal",
259
- currentId: accountId,
260
- listAccountIds: listZalouserAccountIds,
261
- defaultAccountId,
262
- });
263
- }
222
+ const accountId = await resolveAccountIdForConfigure({
223
+ cfg,
224
+ prompter,
225
+ label: "Zalo Personal",
226
+ accountOverride: accountOverrides.zalouser,
227
+ shouldPromptAccountIds,
228
+ listAccountIds: listZalouserAccountIds,
229
+ defaultAccountId,
230
+ });
264
231
 
265
232
  let next = cfg;
266
233
  const account = resolveZalouserAccountSync({ cfg: next, accountId });
package/src/probe.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BaseProbeResult } from "openclaw/plugin-sdk";
1
+ import type { BaseProbeResult } from "openclaw/plugin-sdk/zalouser";
2
2
  import type { ZcaUserInfo } from "./types.js";
3
3
  import { getZaloUserInfo } from "./zalo-js.js";
4
4
 
@@ -0,0 +1,22 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/zalouser";
4
+
5
+ export async function writeQrDataUrlToTempFile(
6
+ qrDataUrl: string,
7
+ profile: string,
8
+ ): Promise<string | null> {
9
+ const trimmed = qrDataUrl.trim();
10
+ const match = trimmed.match(/^data:image\/png;base64,(.+)$/i);
11
+ const base64 = (match?.[1] ?? "").trim();
12
+ if (!base64) {
13
+ return null;
14
+ }
15
+ const safeProfile = profile.replace(/[^a-zA-Z0-9_-]+/g, "-") || "default";
16
+ const filePath = path.join(
17
+ resolvePreferredOpenClawTmpDir(),
18
+ `openclaw-zalouser-qr-${safeProfile}.png`,
19
+ );
20
+ await fsp.writeFile(filePath, Buffer.from(base64, "base64"));
21
+ return filePath;
22
+ }
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/zalouser";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk";
1
+ import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/zalouser";
2
2
 
3
3
  type ZalouserAccountStatus = {
4
4
  accountId?: unknown;
package/src/zalo-js.ts CHANGED
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import fsp from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk";
6
+ import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/zalouser";
7
7
  import { normalizeZaloReactionIcon } from "./reaction.js";
8
8
  import { getZalouserRuntime } from "./runtime.js";
9
9
  import type {
package/src/zca-client.ts CHANGED
@@ -126,6 +126,20 @@ export type Listener = {
126
126
  stop(): void;
127
127
  };
128
128
 
129
+ type DeliveryEventMessage = {
130
+ msgId: string;
131
+ cliMsgId: string;
132
+ uidFrom: string;
133
+ idTo: string;
134
+ msgType: string;
135
+ st: number;
136
+ at: number;
137
+ cmd: number;
138
+ ts: string | number;
139
+ };
140
+
141
+ type DeliveryEventMessages = DeliveryEventMessage | DeliveryEventMessage[];
142
+
129
143
  export type API = {
130
144
  listener: Listener;
131
145
  getContext(): {
@@ -185,57 +199,10 @@ export type API = {
185
199
  ): Promise<unknown>;
186
200
  sendDeliveredEvent(
187
201
  isSeen: boolean,
188
- messages:
189
- | {
190
- msgId: string;
191
- cliMsgId: string;
192
- uidFrom: string;
193
- idTo: string;
194
- msgType: string;
195
- st: number;
196
- at: number;
197
- cmd: number;
198
- ts: string | number;
199
- }
200
- | Array<{
201
- msgId: string;
202
- cliMsgId: string;
203
- uidFrom: string;
204
- idTo: string;
205
- msgType: string;
206
- st: number;
207
- at: number;
208
- cmd: number;
209
- ts: string | number;
210
- }>,
211
- type?: number,
212
- ): Promise<unknown>;
213
- sendSeenEvent(
214
- messages:
215
- | {
216
- msgId: string;
217
- cliMsgId: string;
218
- uidFrom: string;
219
- idTo: string;
220
- msgType: string;
221
- st: number;
222
- at: number;
223
- cmd: number;
224
- ts: string | number;
225
- }
226
- | Array<{
227
- msgId: string;
228
- cliMsgId: string;
229
- uidFrom: string;
230
- idTo: string;
231
- msgType: string;
232
- st: number;
233
- at: number;
234
- cmd: number;
235
- ts: string | number;
236
- }>,
202
+ messages: DeliveryEventMessages,
237
203
  type?: number,
238
204
  ): Promise<unknown>;
205
+ sendSeenEvent(messages: DeliveryEventMessages, type?: number): Promise<unknown>;
239
206
  };
240
207
 
241
208
  type ZaloCtor = new (options?: { logging?: boolean; selfListen?: boolean }) => {