@openclaw/zalo 2026.3.13 → 2026.5.2-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.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +5 -0
  5. package/index.test.ts +15 -0
  6. package/index.ts +16 -13
  7. package/openclaw.plugin.json +514 -1
  8. package/package.json +31 -5
  9. package/runtime-api.test.ts +17 -0
  10. package/runtime-api.ts +75 -0
  11. package/secret-contract-api.ts +5 -0
  12. package/setup-api.ts +34 -0
  13. package/setup-entry.ts +13 -0
  14. package/src/accounts.test.ts +70 -0
  15. package/src/accounts.ts +19 -19
  16. package/src/actions.runtime.ts +5 -0
  17. package/src/actions.test.ts +32 -0
  18. package/src/actions.ts +20 -14
  19. package/src/api.test.ts +93 -2
  20. package/src/api.ts +29 -2
  21. package/src/approval-auth.test.ts +17 -0
  22. package/src/approval-auth.ts +25 -0
  23. package/src/channel.directory.test.ts +19 -6
  24. package/src/channel.runtime.ts +93 -0
  25. package/src/channel.startup.test.ts +26 -19
  26. package/src/channel.ts +229 -336
  27. package/src/config-schema.ts +3 -3
  28. package/src/group-access.ts +4 -3
  29. package/src/monitor.group-policy.test.ts +0 -12
  30. package/src/monitor.image.polling.test.ts +110 -0
  31. package/src/monitor.lifecycle.test.ts +41 -22
  32. package/src/monitor.pairing.lifecycle.test.ts +141 -0
  33. package/src/monitor.polling.media-reply.test.ts +425 -0
  34. package/src/monitor.reply-once.lifecycle.test.ts +171 -0
  35. package/src/monitor.ts +460 -206
  36. package/src/monitor.types.ts +4 -0
  37. package/src/monitor.webhook.test.ts +392 -62
  38. package/src/monitor.webhook.ts +73 -36
  39. package/src/outbound-media.test.ts +182 -0
  40. package/src/outbound-media.ts +241 -0
  41. package/src/outbound-payload.contract.test.ts +45 -0
  42. package/src/probe.ts +1 -1
  43. package/src/proxy.ts +1 -1
  44. package/src/runtime-api.ts +75 -0
  45. package/src/runtime-support.ts +91 -0
  46. package/src/runtime.ts +6 -3
  47. package/src/secret-contract.ts +109 -0
  48. package/src/secret-input.ts +1 -9
  49. package/src/send.test.ts +120 -0
  50. package/src/send.ts +15 -13
  51. package/src/session-route.ts +32 -0
  52. package/src/setup-allow-from.ts +94 -0
  53. package/src/setup-core.ts +149 -0
  54. package/src/{onboarding.status.test.ts → setup-status.test.ts} +13 -4
  55. package/src/setup-surface.test.ts +175 -0
  56. package/src/{onboarding.ts → setup-surface.ts} +59 -177
  57. package/src/status-issues.test.ts +2 -14
  58. package/src/status-issues.ts +8 -2
  59. package/src/test-support/lifecycle-test-support.ts +413 -0
  60. package/src/test-support/monitor-mocks-test-support.ts +209 -0
  61. package/src/token.test.ts +15 -0
  62. package/src/token.ts +8 -17
  63. package/src/types.ts +2 -2
  64. package/test-api.ts +1 -0
  65. package/tsconfig.json +16 -0
  66. package/CHANGELOG.md +0 -101
  67. package/src/channel.sendpayload.test.ts +0 -44
@@ -0,0 +1,175 @@
1
+ import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
2
+ import {
3
+ createPluginSetupWizardConfigure,
4
+ createTestWizardPrompter,
5
+ runSetupWizardConfigure,
6
+ } from "openclaw/plugin-sdk/plugin-test-runtime";
7
+ import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime";
8
+ import { describe, expect, it, vi } from "vitest";
9
+ import type { OpenClawConfig } from "../runtime-api.js";
10
+ import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
11
+ import { zaloDmPolicy } from "./setup-core.js";
12
+ import { zaloSetupAdapter, zaloSetupWizard } from "./setup-surface.js";
13
+
14
+ const zaloSetupPlugin = {
15
+ id: "zalo",
16
+ meta: {
17
+ id: "zalo",
18
+ label: "Zalo",
19
+ selectionLabel: "Zalo (Bot API)",
20
+ docsPath: "/channels/zalo",
21
+ blurb: "Vietnam-focused messaging platform with Bot API.",
22
+ },
23
+ capabilities: {
24
+ chatTypes: ["direct", "group"] as Array<"direct" | "group">,
25
+ },
26
+ config: {
27
+ listAccountIds: (cfg: unknown) => listZaloAccountIds(cfg as never),
28
+ defaultAccountId: (cfg: unknown) => resolveDefaultZaloAccountId(cfg as never),
29
+ resolveAccount: adaptScopedAccountAccessor(resolveZaloAccount),
30
+ },
31
+ setup: zaloSetupAdapter,
32
+ setupWizard: zaloSetupWizard,
33
+ } as const;
34
+
35
+ const zaloConfigure = createPluginSetupWizardConfigure(zaloSetupPlugin);
36
+
37
+ describe("zalo setup wizard", () => {
38
+ it("configures a polling token flow", async () => {
39
+ const prompter = createTestWizardPrompter({
40
+ select: vi.fn(async () => "plaintext") as WizardPrompter["select"],
41
+ text: vi.fn(async ({ message }: { message: string }) => {
42
+ if (message === "Enter Zalo bot token") {
43
+ return "12345689:abc-xyz";
44
+ }
45
+ throw new Error(`Unexpected prompt: ${message}`);
46
+ }) as WizardPrompter["text"],
47
+ confirm: vi.fn(async ({ message }: { message: string }) => {
48
+ if (message === "Use webhook mode for Zalo?") {
49
+ return false;
50
+ }
51
+ return false;
52
+ }),
53
+ });
54
+
55
+ const result = await runSetupWizardConfigure({
56
+ configure: zaloConfigure,
57
+ cfg: {} as OpenClawConfig,
58
+ prompter,
59
+ options: { secretInputMode: "plaintext" as const },
60
+ });
61
+
62
+ expect(result.accountId).toBe("default");
63
+ expect(result.cfg.channels?.zalo?.enabled).toBe(true);
64
+ expect(result.cfg.channels?.zalo?.botToken).toBe("12345689:abc-xyz");
65
+ expect(result.cfg.channels?.zalo?.webhookUrl).toBeUndefined();
66
+ });
67
+
68
+ it("reads the named-account DM policy instead of the channel root", () => {
69
+ expect(
70
+ zaloDmPolicy.getCurrent(
71
+ {
72
+ channels: {
73
+ zalo: {
74
+ dmPolicy: "disabled",
75
+ accounts: {
76
+ work: {
77
+ botToken: "12345689:abc-xyz",
78
+ dmPolicy: "allowlist",
79
+ },
80
+ },
81
+ },
82
+ },
83
+ } as OpenClawConfig,
84
+ "work",
85
+ ),
86
+ ).toBe("allowlist");
87
+ });
88
+
89
+ it("reports account-scoped config keys for named accounts", () => {
90
+ expect(zaloDmPolicy.resolveConfigKeys?.({} as OpenClawConfig, "work")).toEqual({
91
+ policyKey: "channels.zalo.accounts.work.dmPolicy",
92
+ allowFromKey: "channels.zalo.accounts.work.allowFrom",
93
+ });
94
+ });
95
+
96
+ it("uses configured defaultAccount for omitted DM policy account context", () => {
97
+ const cfg = {
98
+ channels: {
99
+ zalo: {
100
+ defaultAccount: "work",
101
+ dmPolicy: "disabled",
102
+ allowFrom: ["123456789"],
103
+ accounts: {
104
+ work: {
105
+ botToken: "12345689:abc-xyz",
106
+ dmPolicy: "allowlist",
107
+ },
108
+ },
109
+ },
110
+ },
111
+ } as OpenClawConfig;
112
+
113
+ expect(zaloDmPolicy.getCurrent(cfg)).toBe("allowlist");
114
+ expect(zaloDmPolicy.resolveConfigKeys?.(cfg)).toEqual({
115
+ policyKey: "channels.zalo.accounts.work.dmPolicy",
116
+ allowFromKey: "channels.zalo.accounts.work.allowFrom",
117
+ });
118
+
119
+ const next = zaloDmPolicy.setPolicy(cfg, "open");
120
+ expect(next.channels?.zalo?.dmPolicy).toBe("disabled");
121
+ const workAccount = next.channels?.zalo?.accounts?.work as
122
+ | { dmPolicy?: string; allowFrom?: Array<string | number> }
123
+ | undefined;
124
+ expect(workAccount?.dmPolicy).toBe("open");
125
+ });
126
+
127
+ it('writes open policy state to the named account and preserves inherited allowFrom with "*"', () => {
128
+ const next = zaloDmPolicy.setPolicy(
129
+ {
130
+ channels: {
131
+ zalo: {
132
+ allowFrom: ["123456789"],
133
+ accounts: {
134
+ work: {
135
+ botToken: "12345689:abc-xyz",
136
+ },
137
+ },
138
+ },
139
+ },
140
+ } as OpenClawConfig,
141
+ "open",
142
+ "work",
143
+ );
144
+
145
+ expect(next.channels?.zalo?.dmPolicy).toBeUndefined();
146
+ const workAccount = next.channels?.zalo?.accounts?.work as
147
+ | { dmPolicy?: string; allowFrom?: Array<string | number> }
148
+ | undefined;
149
+ expect(workAccount?.dmPolicy).toBe("open");
150
+ expect(workAccount?.allowFrom).toEqual(["123456789", "*"]);
151
+ });
152
+
153
+ it("uses configured defaultAccount for omitted setup configured state", async () => {
154
+ const configured = await zaloSetupWizard.status.resolveConfigured({
155
+ cfg: {
156
+ channels: {
157
+ zalo: {
158
+ defaultAccount: "work",
159
+ botToken: "root-token",
160
+ accounts: {
161
+ alerts: {
162
+ botToken: "alerts-token",
163
+ },
164
+ work: {
165
+ botToken: "",
166
+ },
167
+ },
168
+ },
169
+ },
170
+ } as OpenClawConfig,
171
+ });
172
+
173
+ expect(configured).toBe(false);
174
+ });
175
+ });
@@ -1,38 +1,22 @@
1
- import type {
2
- ChannelOnboardingAdapter,
3
- ChannelOnboardingDmPolicy,
4
- OpenClawConfig,
5
- SecretInput,
6
- WizardPrompter,
7
- } from "openclaw/plugin-sdk/zalo";
8
1
  import {
9
2
  buildSingleChannelSecretPromptState,
3
+ createStandardChannelSetupStatus,
10
4
  DEFAULT_ACCOUNT_ID,
11
5
  hasConfiguredSecretInput,
12
- mergeAllowFromEntries,
13
- normalizeAccountId,
14
6
  promptSingleChannelSecretInput,
15
7
  runSingleChannelSecretStep,
16
- resolveAccountIdForConfigure,
17
- setTopLevelChannelDmPolicyWithAllowFrom,
18
- } from "openclaw/plugin-sdk/zalo";
19
- import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
8
+ type ChannelSetupWizard,
9
+ type OpenClawConfig,
10
+ type SecretInput,
11
+ } from "openclaw/plugin-sdk/setup";
12
+ import { resolveZaloAccount } from "./accounts.js";
13
+ import { noteZaloTokenHelp, promptZaloAllowFrom } from "./setup-allow-from.js";
14
+ import { zaloDmPolicy } from "./setup-core.js";
20
15
 
21
16
  const channel = "zalo" as const;
22
17
 
23
18
  type UpdateMode = "polling" | "webhook";
24
19
 
25
- function setZaloDmPolicy(
26
- cfg: OpenClawConfig,
27
- dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
28
- ) {
29
- return setTopLevelChannelDmPolicyWithAllowFrom({
30
- cfg,
31
- channel: "zalo",
32
- dmPolicy,
33
- }) as OpenClawConfig;
34
- }
35
-
36
20
  function setZaloUpdateMode(
37
21
  cfg: OpenClawConfig,
38
22
  accountId: string,
@@ -108,108 +92,22 @@ function setZaloUpdateMode(
108
92
  } as OpenClawConfig;
109
93
  }
110
94
 
111
- async function noteZaloTokenHelp(prompter: WizardPrompter): Promise<void> {
112
- await prompter.note(
113
- [
114
- "1) Open Zalo Bot Platform: https://bot.zaloplatforms.com",
115
- "2) Create a bot and get the token",
116
- "3) Token looks like 12345689:abc-xyz",
117
- "Tip: you can also set ZALO_BOT_TOKEN in your env.",
118
- "Docs: https://docs.openclaw.ai/channels/zalo",
119
- ].join("\n"),
120
- "Zalo bot token",
121
- );
122
- }
95
+ export { zaloSetupAdapter } from "./setup-core.js";
123
96
 
124
- async function promptZaloAllowFrom(params: {
125
- cfg: OpenClawConfig;
126
- prompter: WizardPrompter;
127
- accountId: string;
128
- }): Promise<OpenClawConfig> {
129
- const { cfg, prompter, accountId } = params;
130
- const resolved = resolveZaloAccount({ cfg, accountId });
131
- const existingAllowFrom = resolved.config.allowFrom ?? [];
132
- const entry = await prompter.text({
133
- message: "Zalo allowFrom (user id)",
134
- placeholder: "123456789",
135
- initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
136
- validate: (value) => {
137
- const raw = String(value ?? "").trim();
138
- if (!raw) {
139
- return "Required";
140
- }
141
- if (!/^\d+$/.test(raw)) {
142
- return "Use a numeric Zalo user id";
143
- }
144
- return undefined;
145
- },
146
- });
147
- const normalized = String(entry).trim();
148
- const unique = mergeAllowFromEntries(existingAllowFrom, [normalized]);
149
-
150
- if (accountId === DEFAULT_ACCOUNT_ID) {
151
- return {
152
- ...cfg,
153
- channels: {
154
- ...cfg.channels,
155
- zalo: {
156
- ...cfg.channels?.zalo,
157
- enabled: true,
158
- dmPolicy: "allowlist",
159
- allowFrom: unique,
160
- },
161
- },
162
- } as OpenClawConfig;
163
- }
164
-
165
- return {
166
- ...cfg,
167
- channels: {
168
- ...cfg.channels,
169
- zalo: {
170
- ...cfg.channels?.zalo,
171
- enabled: true,
172
- accounts: {
173
- ...cfg.channels?.zalo?.accounts,
174
- [accountId]: {
175
- ...cfg.channels?.zalo?.accounts?.[accountId],
176
- enabled: cfg.channels?.zalo?.accounts?.[accountId]?.enabled ?? true,
177
- dmPolicy: "allowlist",
178
- allowFrom: unique,
179
- },
180
- },
181
- },
182
- },
183
- } as OpenClawConfig;
184
- }
185
-
186
- const dmPolicy: ChannelOnboardingDmPolicy = {
187
- label: "Zalo",
188
- channel,
189
- policyKey: "channels.zalo.dmPolicy",
190
- allowFromKey: "channels.zalo.allowFrom",
191
- getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
192
- setPolicy: (cfg, policy) => setZaloDmPolicy(cfg, policy),
193
- promptAllowFrom: async ({ cfg, prompter, accountId }) => {
194
- const id =
195
- accountId && normalizeAccountId(accountId)
196
- ? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
197
- : resolveDefaultZaloAccountId(cfg);
198
- return promptZaloAllowFrom({
199
- cfg: cfg,
200
- prompter,
201
- accountId: id,
202
- });
203
- },
204
- };
205
-
206
- export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
97
+ export const zaloSetupWizard: ChannelSetupWizard = {
207
98
  channel,
208
- dmPolicy,
209
- getStatus: async ({ cfg }) => {
210
- const configured = listZaloAccountIds(cfg).some((accountId) => {
99
+ status: createStandardChannelSetupStatus({
100
+ channelLabel: "Zalo",
101
+ configuredLabel: "configured",
102
+ unconfiguredLabel: "needs token",
103
+ configuredHint: "recommended · configured",
104
+ unconfiguredHint: "recommended · newcomer-friendly",
105
+ configuredScore: 1,
106
+ unconfiguredScore: 10,
107
+ includeStatusLine: true,
108
+ resolveConfigured: ({ cfg, accountId }) => {
211
109
  const account = resolveZaloAccount({
212
- cfg: cfg,
110
+ cfg,
213
111
  accountId,
214
112
  allowUnresolvedSecretRef: true,
215
113
  });
@@ -218,41 +116,18 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
218
116
  hasConfiguredSecretInput(account.config.botToken) ||
219
117
  Boolean(account.config.tokenFile?.trim())
220
118
  );
221
- });
222
- return {
223
- channel,
224
- configured,
225
- statusLines: [`Zalo: ${configured ? "configured" : "needs token"}`],
226
- selectionHint: configured ? "recommended · configured" : "recommended · newcomer-friendly",
227
- quickstartScore: configured ? 1 : 10,
228
- };
229
- },
230
- configure: async ({
231
- cfg,
232
- prompter,
233
- accountOverrides,
234
- shouldPromptAccountIds,
235
- forceAllowFrom,
236
- }) => {
237
- const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg);
238
- const zaloAccountId = await resolveAccountIdForConfigure({
239
- cfg,
240
- prompter,
241
- label: "Zalo",
242
- accountOverride: accountOverrides.zalo,
243
- shouldPromptAccountIds,
244
- listAccountIds: listZaloAccountIds,
245
- defaultAccountId: defaultZaloAccountId,
246
- });
247
-
119
+ },
120
+ }),
121
+ credentials: [],
122
+ finalize: async ({ cfg, accountId, forceAllowFrom, options, prompter }) => {
248
123
  let next = cfg;
249
124
  const resolvedAccount = resolveZaloAccount({
250
125
  cfg: next,
251
- accountId: zaloAccountId,
126
+ accountId,
252
127
  allowUnresolvedSecretRef: true,
253
128
  });
254
129
  const accountConfigured = Boolean(resolvedAccount.token);
255
- const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
130
+ const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
256
131
  const hasConfigToken = Boolean(
257
132
  hasConfiguredSecretInput(resolvedAccount.config.botToken) || resolvedAccount.config.tokenFile,
258
133
  );
@@ -261,6 +136,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
261
136
  prompter,
262
137
  providerHint: "zalo",
263
138
  credentialLabel: "bot token",
139
+ secretInputMode: options?.secretInputMode,
264
140
  accountConfigured,
265
141
  hasConfigToken,
266
142
  allowEnv,
@@ -270,43 +146,45 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
270
146
  inputPrompt: "Enter Zalo bot token",
271
147
  preferredEnvVar: "ZALO_BOT_TOKEN",
272
148
  onMissingConfigured: async () => await noteZaloTokenHelp(prompter),
273
- applyUseEnv: async (cfg) =>
274
- zaloAccountId === DEFAULT_ACCOUNT_ID
149
+ applyUseEnv: async (currentCfg) =>
150
+ accountId === DEFAULT_ACCOUNT_ID
275
151
  ? ({
276
- ...cfg,
152
+ ...currentCfg,
277
153
  channels: {
278
- ...cfg.channels,
154
+ ...currentCfg.channels,
279
155
  zalo: {
280
- ...cfg.channels?.zalo,
156
+ ...currentCfg.channels?.zalo,
281
157
  enabled: true,
282
158
  },
283
159
  },
284
160
  } as OpenClawConfig)
285
- : cfg,
286
- applySet: async (cfg, value) =>
287
- zaloAccountId === DEFAULT_ACCOUNT_ID
161
+ : currentCfg,
162
+ applySet: async (currentCfg, value) =>
163
+ accountId === DEFAULT_ACCOUNT_ID
288
164
  ? ({
289
- ...cfg,
165
+ ...currentCfg,
290
166
  channels: {
291
- ...cfg.channels,
167
+ ...currentCfg.channels,
292
168
  zalo: {
293
- ...cfg.channels?.zalo,
169
+ ...currentCfg.channels?.zalo,
294
170
  enabled: true,
295
171
  botToken: value,
296
172
  },
297
173
  },
298
174
  } as OpenClawConfig)
299
175
  : ({
300
- ...cfg,
176
+ ...currentCfg,
301
177
  channels: {
302
- ...cfg.channels,
178
+ ...currentCfg.channels,
303
179
  zalo: {
304
- ...cfg.channels?.zalo,
180
+ ...currentCfg.channels?.zalo,
305
181
  enabled: true,
306
182
  accounts: {
307
- ...cfg.channels?.zalo?.accounts,
308
- [zaloAccountId]: {
309
- ...cfg.channels?.zalo?.accounts?.[zaloAccountId],
183
+ ...currentCfg.channels?.zalo?.accounts,
184
+ [accountId]: {
185
+ ...(currentCfg.channels?.zalo?.accounts?.[accountId] as
186
+ | Record<string, unknown>
187
+ | undefined),
310
188
  enabled: true,
311
189
  botToken: value,
312
190
  },
@@ -322,13 +200,13 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
322
200
  initialValue: Boolean(resolvedAccount.config.webhookUrl),
323
201
  });
324
202
  if (wantsWebhook) {
325
- const webhookUrl = String(
203
+ const webhookUrl = (
326
204
  await prompter.text({
327
205
  message: "Webhook URL (https://...) ",
328
206
  initialValue: resolvedAccount.config.webhookUrl,
329
207
  validate: (value) =>
330
208
  value?.trim()?.startsWith("https://") ? undefined : "HTTPS URL required",
331
- }),
209
+ })
332
210
  ).trim();
333
211
  const defaultPath = (() => {
334
212
  try {
@@ -337,11 +215,13 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
337
215
  return "/zalo-webhook";
338
216
  }
339
217
  })();
218
+
340
219
  let webhookSecretResult = await promptSingleChannelSecretInput({
341
220
  cfg: next,
342
221
  prompter,
343
222
  providerHint: "zalo-webhook",
344
223
  credentialLabel: "webhook secret",
224
+ secretInputMode: options?.secretInputMode,
345
225
  ...buildSingleChannelSecretPromptState({
346
226
  accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
347
227
  hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
@@ -363,6 +243,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
363
243
  prompter,
364
244
  providerHint: "zalo-webhook",
365
245
  credentialLabel: "webhook secret",
246
+ secretInputMode: options?.secretInputMode,
366
247
  ...buildSingleChannelSecretPromptState({
367
248
  accountConfigured: false,
368
249
  hasConfigToken: false,
@@ -378,32 +259,33 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
378
259
  webhookSecretResult.action === "set"
379
260
  ? webhookSecretResult.value
380
261
  : resolvedAccount.config.webhookSecret;
381
- const webhookPath = String(
262
+ const webhookPath = (
382
263
  await prompter.text({
383
264
  message: "Webhook path (optional)",
384
265
  initialValue: resolvedAccount.config.webhookPath ?? defaultPath,
385
- }),
266
+ })
386
267
  ).trim();
387
268
  next = setZaloUpdateMode(
388
269
  next,
389
- zaloAccountId,
270
+ accountId,
390
271
  "webhook",
391
272
  webhookUrl,
392
273
  webhookSecret,
393
274
  webhookPath || undefined,
394
275
  );
395
276
  } else {
396
- next = setZaloUpdateMode(next, zaloAccountId, "polling");
277
+ next = setZaloUpdateMode(next, accountId, "polling");
397
278
  }
398
279
 
399
280
  if (forceAllowFrom) {
400
281
  next = await promptZaloAllowFrom({
401
282
  cfg: next,
402
283
  prompter,
403
- accountId: zaloAccountId,
284
+ accountId,
404
285
  });
405
286
  }
406
287
 
407
- return { cfg: next, accountId: zaloAccountId };
288
+ return { cfg: next };
408
289
  },
290
+ dmPolicy: zaloDmPolicy,
409
291
  };
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "vitest";
2
- import { expectOpenDmPolicyConfigIssue } from "../../test-utils/status-issues.js";
1
+ import { expectOpenDmPolicyConfigIssue } from "openclaw/plugin-sdk/channel-test-helpers";
2
+ import { describe, it } from "vitest";
3
3
  import { collectZaloStatusIssues } from "./status-issues.js";
4
4
 
5
5
  describe("collectZaloStatusIssues", () => {
@@ -14,16 +14,4 @@ describe("collectZaloStatusIssues", () => {
14
14
  },
15
15
  });
16
16
  });
17
-
18
- it("skips unconfigured accounts", () => {
19
- const issues = collectZaloStatusIssues([
20
- {
21
- accountId: "default",
22
- enabled: true,
23
- configured: false,
24
- dmPolicy: "open",
25
- },
26
- ]);
27
- expect(issues).toHaveLength(0);
28
- });
29
17
  });
@@ -1,5 +1,11 @@
1
- import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/zalo";
2
- import { coerceStatusIssueAccountId, readStatusIssueFields } from "../../shared/status-issues.js";
1
+ import type {
2
+ ChannelAccountSnapshot,
3
+ ChannelStatusIssue,
4
+ } from "openclaw/plugin-sdk/channel-contract";
5
+ import {
6
+ coerceStatusIssueAccountId,
7
+ readStatusIssueFields,
8
+ } from "openclaw/plugin-sdk/extension-shared";
3
9
 
4
10
  const ZALO_STATUS_FIELDS = ["accountId", "enabled", "configured", "dmPolicy"] as const;
5
11