@openclaw/zalo 2026.3.2 → 2026.3.8-beta.1

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/src/onboarding.ts CHANGED
@@ -4,16 +4,17 @@ import type {
4
4
  OpenClawConfig,
5
5
  SecretInput,
6
6
  WizardPrompter,
7
- } from "openclaw/plugin-sdk";
7
+ } from "openclaw/plugin-sdk/zalo";
8
8
  import {
9
- addWildcardAllowFrom,
9
+ buildSingleChannelSecretPromptState,
10
10
  DEFAULT_ACCOUNT_ID,
11
11
  hasConfiguredSecretInput,
12
12
  mergeAllowFromEntries,
13
13
  normalizeAccountId,
14
- promptAccountId,
15
14
  promptSingleChannelSecretInput,
16
- } from "openclaw/plugin-sdk";
15
+ resolveAccountIdForConfigure,
16
+ setTopLevelChannelDmPolicyWithAllowFrom,
17
+ } from "openclaw/plugin-sdk/zalo";
17
18
  import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
18
19
 
19
20
  const channel = "zalo" as const;
@@ -24,19 +25,11 @@ function setZaloDmPolicy(
24
25
  cfg: OpenClawConfig,
25
26
  dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
26
27
  ) {
27
- const allowFrom =
28
- dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalo?.allowFrom) : undefined;
29
- return {
30
- ...cfg,
31
- channels: {
32
- ...cfg.channels,
33
- zalo: {
34
- ...cfg.channels?.zalo,
35
- dmPolicy,
36
- ...(allowFrom ? { allowFrom } : {}),
37
- },
38
- },
39
- } as OpenClawConfig;
28
+ return setTopLevelChannelDmPolicyWithAllowFrom({
29
+ cfg,
30
+ channel: "zalo",
31
+ dmPolicy,
32
+ }) as OpenClawConfig;
40
33
  }
41
34
 
42
35
  function setZaloUpdateMode(
@@ -240,19 +233,16 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
240
233
  shouldPromptAccountIds,
241
234
  forceAllowFrom,
242
235
  }) => {
243
- const zaloOverride = accountOverrides.zalo?.trim();
244
236
  const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg);
245
- let zaloAccountId = zaloOverride ? normalizeAccountId(zaloOverride) : defaultZaloAccountId;
246
- if (shouldPromptAccountIds && !zaloOverride) {
247
- zaloAccountId = await promptAccountId({
248
- cfg: cfg,
249
- prompter,
250
- label: "Zalo",
251
- currentId: zaloAccountId,
252
- listAccountIds: listZaloAccountIds,
253
- defaultAccountId: defaultZaloAccountId,
254
- });
255
- }
237
+ const zaloAccountId = await resolveAccountIdForConfigure({
238
+ cfg,
239
+ prompter,
240
+ label: "Zalo",
241
+ accountOverride: accountOverrides.zalo,
242
+ shouldPromptAccountIds,
243
+ listAccountIds: listZaloAccountIds,
244
+ defaultAccountId: defaultZaloAccountId,
245
+ });
256
246
 
257
247
  let next = cfg;
258
248
  const resolvedAccount = resolveZaloAccount({
@@ -262,10 +252,15 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
262
252
  });
263
253
  const accountConfigured = Boolean(resolvedAccount.token);
264
254
  const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
265
- const canUseEnv = allowEnv && Boolean(process.env.ZALO_BOT_TOKEN?.trim());
266
255
  const hasConfigToken = Boolean(
267
256
  hasConfiguredSecretInput(resolvedAccount.config.botToken) || resolvedAccount.config.tokenFile,
268
257
  );
258
+ const tokenPromptState = buildSingleChannelSecretPromptState({
259
+ accountConfigured,
260
+ hasConfigToken,
261
+ allowEnv,
262
+ envValue: process.env.ZALO_BOT_TOKEN,
263
+ });
269
264
 
270
265
  let token: SecretInput | null = null;
271
266
  if (!accountConfigured) {
@@ -276,9 +271,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
276
271
  prompter,
277
272
  providerHint: "zalo",
278
273
  credentialLabel: "bot token",
279
- accountConfigured,
280
- canUseEnv: canUseEnv && !hasConfigToken,
281
- hasConfigToken,
274
+ accountConfigured: tokenPromptState.accountConfigured,
275
+ canUseEnv: tokenPromptState.canUseEnv,
276
+ hasConfigToken: tokenPromptState.hasConfigToken,
282
277
  envPrompt: "ZALO_BOT_TOKEN detected. Use env var?",
283
278
  keepPrompt: "Zalo token already configured. Keep it?",
284
279
  inputPrompt: "Enter Zalo bot token",
@@ -360,9 +355,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
360
355
  prompter,
361
356
  providerHint: "zalo-webhook",
362
357
  credentialLabel: "webhook secret",
363
- accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
364
- canUseEnv: false,
365
- hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
358
+ ...buildSingleChannelSecretPromptState({
359
+ accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
360
+ hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
361
+ allowEnv: false,
362
+ }),
366
363
  envPrompt: "",
367
364
  keepPrompt: "Zalo webhook secret already configured. Keep it?",
368
365
  inputPrompt: "Webhook secret (8-256 chars)",
@@ -379,9 +376,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
379
376
  prompter,
380
377
  providerHint: "zalo-webhook",
381
378
  credentialLabel: "webhook secret",
382
- accountConfigured: false,
383
- canUseEnv: false,
384
- hasConfigToken: false,
379
+ ...buildSingleChannelSecretPromptState({
380
+ accountConfigured: false,
381
+ hasConfigToken: false,
382
+ allowEnv: false,
383
+ }),
385
384
  envPrompt: "",
386
385
  keepPrompt: "Zalo webhook secret already configured. Keep it?",
387
386
  inputPrompt: "Webhook secret (8-256 chars)",
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/zalo";
2
2
  import { getMe, ZaloApiError, type ZaloBotInfo, type ZaloFetch } from "./api.js";
3
3
 
4
4
  export type ZaloProbeResult = BaseProbeResult<string> & {
package/src/runtime.ts CHANGED
@@ -1,14 +1,6 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
2
+ import type { PluginRuntime } from "openclaw/plugin-sdk/zalo";
2
3
 
3
- let runtime: PluginRuntime | null = null;
4
-
5
- export function setZaloRuntime(next: PluginRuntime): void {
6
- runtime = next;
7
- }
8
-
9
- export function getZaloRuntime(): PluginRuntime {
10
- if (!runtime) {
11
- throw new Error("Zalo runtime not initialized");
12
- }
13
- return runtime;
14
- }
4
+ const { setRuntime: setZaloRuntime, getRuntime: getZaloRuntime } =
5
+ createPluginRuntimeStore<PluginRuntime>("Zalo runtime not initialized");
6
+ export { getZaloRuntime, setZaloRuntime };
@@ -1,19 +1,13 @@
1
1
  import {
2
+ buildSecretInputSchema,
2
3
  hasConfiguredSecretInput,
3
4
  normalizeResolvedSecretInputString,
4
5
  normalizeSecretInputString,
5
- } from "openclaw/plugin-sdk";
6
- import { z } from "zod";
6
+ } from "openclaw/plugin-sdk/zalo";
7
7
 
8
- export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
9
-
10
- export function buildSecretInputSchema() {
11
- return z.union([
12
- z.string(),
13
- z.object({
14
- source: z.enum(["env", "file", "exec"]),
15
- provider: z.string().min(1),
16
- id: z.string().min(1),
17
- }),
18
- ]);
19
- }
8
+ export {
9
+ buildSecretInputSchema,
10
+ hasConfiguredSecretInput,
11
+ normalizeResolvedSecretInputString,
12
+ normalizeSecretInputString,
13
+ };
package/src/send.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo";
2
2
  import { resolveZaloAccount } from "./accounts.js";
3
3
  import type { ZaloFetch } from "./api.js";
4
4
  import { sendMessage, sendPhoto } from "./api.js";
@@ -40,37 +40,47 @@ function resolveSendContext(options: ZaloSendOptions): {
40
40
  return { token, fetcher: resolveZaloProxyFetch(proxy) };
41
41
  }
42
42
 
43
- export async function sendMessageZalo(
43
+ function resolveValidatedSendContext(
44
44
  chatId: string,
45
- text: string,
46
- options: ZaloSendOptions = {},
47
- ): Promise<ZaloSendResult> {
45
+ options: ZaloSendOptions,
46
+ ): { ok: true; chatId: string; token: string; fetcher?: ZaloFetch } | { ok: false; error: string } {
48
47
  const { token, fetcher } = resolveSendContext(options);
49
-
50
48
  if (!token) {
51
49
  return { ok: false, error: "No Zalo bot token configured" };
52
50
  }
53
-
54
- if (!chatId?.trim()) {
51
+ const trimmedChatId = chatId?.trim();
52
+ if (!trimmedChatId) {
55
53
  return { ok: false, error: "No chat_id provided" };
56
54
  }
55
+ return { ok: true, chatId: trimmedChatId, token, fetcher };
56
+ }
57
+
58
+ export async function sendMessageZalo(
59
+ chatId: string,
60
+ text: string,
61
+ options: ZaloSendOptions = {},
62
+ ): Promise<ZaloSendResult> {
63
+ const context = resolveValidatedSendContext(chatId, options);
64
+ if (!context.ok) {
65
+ return { ok: false, error: context.error };
66
+ }
57
67
 
58
68
  if (options.mediaUrl) {
59
- return sendPhotoZalo(chatId, options.mediaUrl, {
69
+ return sendPhotoZalo(context.chatId, options.mediaUrl, {
60
70
  ...options,
61
- token,
71
+ token: context.token,
62
72
  caption: text || options.caption,
63
73
  });
64
74
  }
65
75
 
66
76
  try {
67
77
  const response = await sendMessage(
68
- token,
78
+ context.token,
69
79
  {
70
- chat_id: chatId.trim(),
80
+ chat_id: context.chatId,
71
81
  text: text.slice(0, 2000),
72
82
  },
73
- fetcher,
83
+ context.fetcher,
74
84
  );
75
85
 
76
86
  if (response.ok && response.result) {
@@ -88,14 +98,9 @@ export async function sendPhotoZalo(
88
98
  photoUrl: string,
89
99
  options: ZaloSendOptions = {},
90
100
  ): Promise<ZaloSendResult> {
91
- const { token, fetcher } = resolveSendContext(options);
92
-
93
- if (!token) {
94
- return { ok: false, error: "No Zalo bot token configured" };
95
- }
96
-
97
- if (!chatId?.trim()) {
98
- return { ok: false, error: "No chat_id provided" };
101
+ const context = resolveValidatedSendContext(chatId, options);
102
+ if (!context.ok) {
103
+ return { ok: false, error: context.error };
99
104
  }
100
105
 
101
106
  if (!photoUrl?.trim()) {
@@ -104,13 +109,13 @@ export async function sendPhotoZalo(
104
109
 
105
110
  try {
106
111
  const response = await sendPhoto(
107
- token,
112
+ context.token,
108
113
  {
109
- chat_id: chatId.trim(),
114
+ chat_id: context.chatId,
110
115
  photo: photoUrl.trim(),
111
116
  caption: options.caption?.slice(0, 2000),
112
117
  },
113
- fetcher,
118
+ context.fetcher,
114
119
  );
115
120
 
116
121
  if (response.ok && response.result) {
@@ -1,4 +1,4 @@
1
- import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk";
1
+ import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/zalo";
2
2
 
3
3
  type ZaloAccountStatus = {
4
4
  accountId?: unknown;
package/src/token.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
- import type { BaseTokenResolution } from "openclaw/plugin-sdk";
3
2
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
+ import type { BaseTokenResolution } from "openclaw/plugin-sdk/zalo";
4
4
  import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
5
5
  import type { ZaloConfig } from "./types.js";
6
6
 
@@ -8,6 +8,19 @@ export type ZaloTokenResolution = BaseTokenResolution & {
8
8
  source: "env" | "config" | "configFile" | "none";
9
9
  };
10
10
 
11
+ function readTokenFromFile(tokenFile: string | undefined): string {
12
+ const trimmedPath = tokenFile?.trim();
13
+ if (!trimmedPath) {
14
+ return "";
15
+ }
16
+ try {
17
+ return readFileSync(trimmedPath, "utf8").trim();
18
+ } catch {
19
+ // ignore read failures
20
+ return "";
21
+ }
22
+ }
23
+
11
24
  export function resolveZaloToken(
12
25
  config: ZaloConfig | undefined,
13
26
  accountId?: string | null,
@@ -44,28 +57,16 @@ export function resolveZaloToken(
44
57
  if (token) {
45
58
  return { token, source: "config" };
46
59
  }
47
- const tokenFile = accountConfig.tokenFile?.trim();
48
- if (tokenFile) {
49
- try {
50
- const fileToken = readFileSync(tokenFile, "utf8").trim();
51
- if (fileToken) {
52
- return { token: fileToken, source: "configFile" };
53
- }
54
- } catch {
55
- // ignore read failures
56
- }
60
+ const fileToken = readTokenFromFile(accountConfig.tokenFile);
61
+ if (fileToken) {
62
+ return { token: fileToken, source: "configFile" };
57
63
  }
58
64
  }
59
65
 
60
- const accountTokenFile = accountConfig?.tokenFile?.trim();
61
- if (!accountHasBotToken && accountTokenFile) {
62
- try {
63
- const fileToken = readFileSync(accountTokenFile, "utf8").trim();
64
- if (fileToken) {
65
- return { token: fileToken, source: "configFile" };
66
- }
67
- } catch {
68
- // ignore read failures
66
+ if (!accountHasBotToken) {
67
+ const fileToken = readTokenFromFile(accountConfig?.tokenFile);
68
+ if (fileToken) {
69
+ return { token: fileToken, source: "configFile" };
69
70
  }
70
71
  }
71
72
 
@@ -79,16 +80,9 @@ export function resolveZaloToken(
79
80
  if (token) {
80
81
  return { token, source: "config" };
81
82
  }
82
- const tokenFile = baseConfig?.tokenFile?.trim();
83
- if (tokenFile) {
84
- try {
85
- const fileToken = readFileSync(tokenFile, "utf8").trim();
86
- if (fileToken) {
87
- return { token: fileToken, source: "configFile" };
88
- }
89
- } catch {
90
- // ignore read failures
91
- }
83
+ const fileToken = readTokenFromFile(baseConfig?.tokenFile);
84
+ if (fileToken) {
85
+ return { token: fileToken, source: "configFile" };
92
86
  }
93
87
  }
94
88
 
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SecretInput } from "openclaw/plugin-sdk";
1
+ import type { SecretInput } from "openclaw/plugin-sdk/zalo";
2
2
 
3
3
  export type ZaloAccountConfig = {
4
4
  /** Optional display name for this account (used in CLI/UI lists). */