@openclaw/zalo 2026.7.2-beta.5 → 2026.8.1-beta.2

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.
@@ -1,4 +1,4 @@
1
- import { t as sendMessageZalo } from "./send-BeG5dH7I.js";
1
+ import { t as sendMessageZalo } from "./send-BGGtv8x_.js";
2
2
  //#region extensions/zalo/src/actions.runtime.ts
3
3
  const zaloActionsRuntime = { sendMessageZalo };
4
4
  //#endregion
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-qcMoaIyL.js";
2
- import { t as zaloPlugin } from "./channel-BqA9TbSR.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-T6qc2eAG.js";
2
+ import { t as zaloPlugin } from "./channel-Dcp-XLf8.js";
3
3
  import { n as resolveZaloRuntimeGroupPolicy } from "./group-access-8qHRzDHx.js";
4
4
  import { zaloSetupWizard } from "./setup-api.js";
5
5
  export { createZaloSetupWizardProxy, resolveZaloRuntimeGroupPolicy, zaloDmPolicy, zaloPlugin, zaloSetupAdapter, zaloSetupWizard };
@@ -1,4 +1,4 @@
1
- import { c as listEnabledZaloAccounts, d as resolveZaloAccount, i as zaloSetupContract, l as listZaloAccountIds, p as buildSecretInputSchema, r as zaloSetupAdapter, t as createZaloSetupWizardProxy, u as resolveDefaultZaloAccountId } from "./setup-core-qcMoaIyL.js";
1
+ import { c as inspectZaloAccount, d as resolveZaloAccount, i as zaloSetupContract, l as listZaloAccountIds, p as buildSecretInputSchema, t as createZaloSetupWizardProxy, u as resolveDefaultZaloAccountId } from "./setup-core-T6qc2eAG.js";
2
2
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-Bxvjh0Hq.js";
3
3
  import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
4
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
@@ -24,14 +24,17 @@ import { AllowFromListSchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigS
24
24
  import { z } from "zod";
25
25
  import { coerceStatusIssueAccountId, readStatusIssueFields } from "openclaw/plugin-sdk/extension-shared";
26
26
  //#region extensions/zalo/src/actions.ts
27
- const loadZaloActionsRuntime = createLazyRuntimeNamedExport(() => import("./actions.runtime-Cqmv6cPB.js"), "zaloActionsRuntime");
27
+ const loadZaloActionsRuntime = createLazyRuntimeNamedExport(() => import("./actions.runtime-BRP15DNe.js"), "zaloActionsRuntime");
28
28
  const providerId = "zalo";
29
29
  const ZALO_ACTIONS = /* @__PURE__ */ new Set(["send"]);
30
30
  function listEnabledAccounts(cfg, accountId) {
31
- return (accountId ? [resolveZaloAccount({
31
+ return (accountId ? [inspectZaloAccount({
32
32
  cfg,
33
33
  accountId
34
- })] : listEnabledZaloAccounts(cfg)).filter((account) => account.enabled && account.tokenSource !== "none");
34
+ })] : listZaloAccountIds(cfg).map((listedAccountId) => inspectZaloAccount({
35
+ cfg,
36
+ accountId: listedAccountId
37
+ }))).filter((account) => account.enabled && account.tokenStatus === "available");
35
38
  }
36
39
  const zaloMessageActions = {
37
40
  describeMessageTool: ({ cfg, accountId }) => {
@@ -89,6 +92,7 @@ const zaloApprovalAuth = createChannelApprovalAuth({
89
92
  const ZaloConfigSchema = buildMultiAccountChannelSchema(z.object({
90
93
  name: z.string().optional(),
91
94
  enabled: z.boolean().optional(),
95
+ configWrites: z.boolean().optional(),
92
96
  markdown: MarkdownConfigSchema,
93
97
  botToken: buildSecretInputSchema().optional(),
94
98
  tokenFile: z.string().optional(),
@@ -176,8 +180,8 @@ function looksLikeZaloChatId(raw, normalized) {
176
180
  const target = normalizeZaloMessagingTarget(normalized ?? raw);
177
181
  return Boolean(target);
178
182
  }
179
- const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-D4wDYzqp.js"));
180
- const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-Cqevy46r.js")).zaloSetupWizard);
183
+ const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-5CivhE5e.js"));
184
+ const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-edfRk3eu.js").then((n) => n.t)).zaloSetupWizard);
181
185
  const zaloTextChunkLimit = 2e3;
182
186
  async function sendZaloDelivery(ctx) {
183
187
  const result = await (await loadZaloChannelRuntime()).sendZaloText({
@@ -210,6 +214,9 @@ const zaloMessageAdapter = defineChannelMessageAdapter({
210
214
  media: sendZaloDelivery
211
215
  }
212
216
  });
217
+ function isZaloAccountConfigured(account) {
218
+ return account.tokenStatus ? account.tokenStatus !== "missing" : Boolean(account.token?.trim());
219
+ }
213
220
  const zaloConfigAdapter = createScopedChannelConfigAdapter({
214
221
  sectionKey: "zalo",
215
222
  listAccountIds: listZaloAccountIds,
@@ -257,7 +264,6 @@ const zaloPlugin = createChatChannelPlugin({
257
264
  base: {
258
265
  id: "zalo",
259
266
  meta,
260
- setup: zaloSetupAdapter,
261
267
  setupContract: zaloSetupContract,
262
268
  setupWizard: zaloSetupWizard,
263
269
  capabilities: {
@@ -273,12 +279,16 @@ const zaloPlugin = createChatChannelPlugin({
273
279
  configSchema: buildChannelConfigSchema(ZaloConfigSchema),
274
280
  config: {
275
281
  ...zaloConfigAdapter,
276
- isConfigured: (account) => Boolean(account.token?.trim()),
282
+ inspectAccount: adaptScopedAccountAccessor(inspectZaloAccount),
283
+ isConfigured: isZaloAccountConfigured,
277
284
  describeAccount: (account) => describeWebhookAccountSnapshot({
278
285
  account,
279
- configured: Boolean(account.token?.trim()),
286
+ configured: isZaloAccountConfigured(account),
280
287
  mode: account.config.webhookUrl ? "webhook" : "polling",
281
- extra: { tokenSource: account.tokenSource }
288
+ extra: {
289
+ tokenSource: account.tokenSource,
290
+ tokenStatus: account.tokenStatus
291
+ }
282
292
  })
283
293
  },
284
294
  approvalCapability: zaloApprovalAuth,
@@ -291,6 +301,10 @@ const zaloPlugin = createChatChannelPlugin({
291
301
  messaging: {
292
302
  targetPrefixes: ["zalo", "zl"],
293
303
  normalizeTarget: normalizeZaloMessagingTarget,
304
+ inferTargetChatType: ({ to }) => {
305
+ const target = normalizeZaloMessagingTarget(to);
306
+ return target ? /^group:/i.test(target) ? "group" : "direct" : void 0;
307
+ },
294
308
  resolveOutboundSessionRoute: (params) => resolveZaloOutboundSessionRoute(params),
295
309
  targetResolver: {
296
310
  looksLikeId: looksLikeZaloChatId,
@@ -315,7 +329,7 @@ const zaloPlugin = createChatChannelPlugin({
315
329
  timeoutMs
316
330
  }),
317
331
  resolveAccountSnapshot: ({ account }) => {
318
- const configured = Boolean(account.token?.trim());
332
+ const configured = isZaloAccountConfigured(account);
319
333
  return {
320
334
  accountId: account.accountId,
321
335
  name: account.name,
@@ -323,6 +337,7 @@ const zaloPlugin = createChatChannelPlugin({
323
337
  configured,
324
338
  extra: {
325
339
  tokenSource: account.tokenSource,
340
+ tokenStatus: account.tokenStatus,
326
341
  mode: account.config.webhookUrl ? "webhook" : "polling",
327
342
  dmPolicy: account.config.dmPolicy ?? "pairing"
328
343
  }
@@ -1,2 +1,2 @@
1
- import { t as zaloPlugin } from "./channel-BqA9TbSR.js";
1
+ import { t as zaloPlugin } from "./channel-Dcp-XLf8.js";
2
2
  export { zaloPlugin };
@@ -1,58 +1,41 @@
1
- import { m as normalizeSecretInputString } from "./setup-core-qcMoaIyL.js";
1
+ import { m as normalizeSecretInputString } from "./setup-core-T6qc2eAG.js";
2
2
  import { i as PAIRING_APPROVED_MESSAGE } from "./runtime-api-qxdOyk3M.js";
3
- import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-BeG5dH7I.js";
3
+ import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-BGGtv8x_.js";
4
4
  import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-outbound";
5
+ import { runChannelProbe } from "openclaw/plugin-sdk/text-utility-runtime";
5
6
  //#region extensions/zalo/src/probe.ts
7
+ function formatZaloProbeError(error, timeoutMs) {
8
+ if (error instanceof ZaloApiError) return error.description ?? error.message;
9
+ if (error instanceof Error) return error.name === "AbortError" ? `Request timed out after ${timeoutMs}ms` : error.message;
10
+ return String(error);
11
+ }
6
12
  async function probeZalo(token, timeoutMs = 5e3, fetcher) {
7
13
  if (!token?.trim()) return {
8
14
  ok: false,
9
15
  error: "No token provided",
10
16
  elapsedMs: 0
11
17
  };
12
- const startTime = Date.now();
13
- try {
18
+ return await runChannelProbe(void 0, async ({ elapsedMs }) => {
14
19
  const response = await getMe(token.trim(), timeoutMs, fetcher);
15
- const elapsedMs = Date.now() - startTime;
16
20
  if (response.ok && response.result) return {
17
21
  ok: true,
18
22
  bot: response.result,
19
- elapsedMs
23
+ elapsedMs: elapsedMs()
20
24
  };
21
25
  return {
22
26
  ok: false,
23
27
  error: "Invalid response from Zalo API",
24
- elapsedMs
25
- };
26
- } catch (err) {
27
- const elapsedMs = Date.now() - startTime;
28
- if (err instanceof ZaloApiError) return {
29
- ok: false,
30
- error: err.description ?? err.message,
31
- elapsedMs
28
+ elapsedMs: elapsedMs()
32
29
  };
33
- if (err instanceof Error) {
34
- if (err.name === "AbortError") return {
35
- ok: false,
36
- error: `Request timed out after ${timeoutMs}ms`,
37
- elapsedMs
38
- };
39
- return {
40
- ok: false,
41
- error: err.message,
42
- elapsedMs
43
- };
44
- }
45
- return {
46
- ok: false,
47
- error: String(err),
48
- elapsedMs
49
- };
50
- }
30
+ }, (error) => ({
31
+ ok: false,
32
+ error: formatZaloProbeError(error, timeoutMs)
33
+ }));
51
34
  }
52
35
  //#endregion
53
36
  //#region extensions/zalo/src/channel.runtime.ts
54
37
  async function notifyZaloPairingApproval(params) {
55
- const { resolveZaloAccount } = await import("./setup-core-qcMoaIyL.js").then((n) => n.s);
38
+ const { resolveZaloAccount } = await import("./setup-core-T6qc2eAG.js").then((n) => n.s);
56
39
  const account = resolveZaloAccount({ cfg: params.cfg });
57
40
  if (!account.token) throw new Error("Zalo token not configured");
58
41
  await sendMessageZalo(params.id, PAIRING_APPROVED_MESSAGE, { token: account.token });
@@ -86,7 +69,7 @@ async function startZaloGatewayAccount(ctx) {
86
69
  setStatus: ctx.setStatus
87
70
  });
88
71
  ctx.log?.info(`[${account.accountId}] starting provider${zaloBotLabel} mode=${mode}`);
89
- const { monitorZaloProvider } = await import("./monitor-DvrLTTtn.js");
72
+ const { monitorZaloProvider } = await import("./monitor-Ckz5Mo22.js");
90
73
  return monitorZaloProvider({
91
74
  token,
92
75
  account,
@@ -1,16 +1,18 @@
1
1
  import { n as resolveZaloRuntimeGroupPolicy, t as normalizeZaloAllowEntry } from "./group-access-8qHRzDHx.js";
2
2
  import { t as getZaloRuntime } from "./runtime-api-qxdOyk3M.js";
3
- import { c as sendChatAction, d as setWebhook, f as ZALO_OUTBOUND_MEDIA_TTL_MS, i as deleteWebhook, l as sendMessage, n as resolveZaloProxyFetch, o as getUpdates, r as ZaloApiError, s as getWebhookInfo, u as sendPhoto } from "./send-BeG5dH7I.js";
3
+ import { c as sendChatAction, d as setWebhook, f as ZALO_OUTBOUND_MEDIA_TTL_MS, i as deleteWebhook, l as sendMessage, n as resolveZaloProxyFetch, o as getUpdates, r as ZaloApiError, s as getWebhookInfo, u as sendPhoto } from "./send-BGGtv8x_.js";
4
+ import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
4
5
  import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
5
6
  import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
6
7
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
8
  import { resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
8
9
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
9
10
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
10
- import { registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
11
+ import { canonicalizeWebhookRouteKey, registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
11
12
  import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
12
- import { formatInboundMediaUnavailableText, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
13
+ import { createChannelPartialDeliveryError, formatInboundMediaUnavailableText, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
13
14
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
15
+ import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
14
16
  import { sleepWithAbort, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
15
17
  import { createHostedOutboundMediaStore } from "openclaw/plugin-sdk/outbound-media";
16
18
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
@@ -115,35 +117,44 @@ async function tryHandleHostedZaloMediaRequest(req, res) {
115
117
  res.end("Expired");
116
118
  return true;
117
119
  }
118
- const entry = await store.read(id, now);
119
- if (!entry || entry.metadata.routePath !== routePath) {
120
+ const metadata = await store.readMetadata(id, now);
121
+ if (!metadata || metadata.routePath !== routePath) {
120
122
  res.statusCode = 404;
121
123
  res.end("Not Found");
122
124
  return true;
123
125
  }
124
- const expiresAt = asDateTimestampMs(entry.metadata.expiresAt);
126
+ const expiresAt = asDateTimestampMs(metadata.expiresAt);
125
127
  if (expiresAt === void 0 || expiresAt <= now) {
126
128
  await store.delete(id);
127
129
  res.statusCode = 410;
128
130
  res.end("Expired");
129
131
  return true;
130
132
  }
131
- if (!safeEqualSecret(url.searchParams.get("token"), entry.metadata.token)) {
133
+ const token = url.searchParams.get("token");
134
+ if (!safeEqualSecret(token, metadata.token)) {
132
135
  res.statusCode = 401;
133
136
  res.end("Unauthorized");
134
137
  return true;
135
138
  }
136
- if (entry.metadata.contentType) res.setHeader("Content-Type", entry.metadata.contentType);
139
+ let servedMetadata = metadata;
140
+ let body;
141
+ if (method === "GET") {
142
+ const entry = await store.read(id, now);
143
+ if (!entry || entry.metadata.routePath !== routePath || !safeEqualSecret(token, entry.metadata.token)) {
144
+ res.statusCode = 404;
145
+ res.end("Not Found");
146
+ return true;
147
+ }
148
+ servedMetadata = entry.metadata;
149
+ body = entry.buffer;
150
+ }
151
+ if (servedMetadata.contentType) res.setHeader("Content-Type", servedMetadata.contentType);
137
152
  res.setHeader("Cache-Control", "no-store");
138
153
  res.setHeader("X-Content-Type-Options", "nosniff");
139
- res.setHeader("Content-Length", String(entry.metadata.byteLength));
140
- if (method === "HEAD") {
141
- res.statusCode = 200;
142
- res.end();
143
- return true;
144
- }
154
+ res.setHeader("Content-Length", String(servedMetadata.byteLength));
145
155
  res.statusCode = 200;
146
- res.end(entry.buffer);
156
+ res.end(body);
157
+ if (method === "HEAD") return true;
147
158
  await store.delete(id);
148
159
  return true;
149
160
  }
@@ -163,31 +174,23 @@ function resolveZaloTimestampMs(date) {
163
174
  if (!date) return;
164
175
  return date >= UNIX_MILLISECONDS_THRESHOLD ? date : date * 1e3;
165
176
  }
166
- const loadZaloWebhookRuntime = createLazyRuntimeNamedExport(() => import("./monitor.webhook-BfWVHoMH.js"), "zaloWebhookRuntime");
167
- const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-D9q-q0oh.js"), "zaloWebhookIngressRuntime");
177
+ const loadZaloWebhookRuntime = createLazyRuntimeNamedExport(() => import("./monitor.webhook-S7nThy3R.js"), "zaloWebhookRuntime");
178
+ const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-Bf17xwqi.js"), "zaloWebhookIngressRuntime");
168
179
  const loadZaloWebhookModule = createLazyRuntimeModule(async () => ({
169
180
  ...await loadZaloWebhookRuntime(),
170
181
  ...await loadZaloWebhookIngressRuntime()
171
182
  }));
172
- function releaseSharedHostedMediaRouteRef(routePath) {
173
- const current = hostedMediaRouteRefs.get(routePath);
174
- if (!current) return;
175
- if (current.count > 1) {
176
- current.count -= 1;
177
- return;
178
- }
179
- hostedMediaRouteRefs.delete(routePath);
180
- for (const unregisterHandle of current.unregisters) unregisterHandle();
181
- }
182
183
  function registerSharedHostedMediaRoute(params) {
184
+ const routeKey = canonicalizeWebhookRouteKey(params.path);
183
185
  const unregister = registerPluginHttpRoute({
184
186
  auth: "plugin",
185
187
  match: "prefix",
186
188
  path: params.path,
187
189
  pluginId: "zalo",
188
190
  source: "zalo-hosted-media",
189
- accountId: params.accountId,
190
191
  log: params.log,
192
+ reuseExistingSameOwner: true,
193
+ throwOnFailure: true,
191
194
  handler: async (req, res) => {
192
195
  if (!await tryHandleHostedZaloMediaRequest(req, res) && !res.headersSent) {
193
196
  res.statusCode = 404;
@@ -196,17 +199,23 @@ function registerSharedHostedMediaRoute(params) {
196
199
  }
197
200
  }
198
201
  });
199
- const existing = hostedMediaRouteRefs.get(params.path);
200
- if (existing) {
201
- existing.count += 1;
202
- existing.unregisters.push(unregister);
203
- return () => releaseSharedHostedMediaRouteRef(params.path);
204
- }
205
- hostedMediaRouteRefs.set(params.path, {
206
- count: 1,
207
- unregisters: [unregister]
208
- });
209
- return () => releaseSharedHostedMediaRouteRef(params.path);
202
+ const acquired = hostedMediaRouteRefs.get(routeKey) ?? {
203
+ count: 0,
204
+ unregisters: []
205
+ };
206
+ if (acquired.count === 0) hostedMediaRouteRefs.set(routeKey, acquired);
207
+ acquired.count += 1;
208
+ acquired.unregisters.push(unregister);
209
+ let released = false;
210
+ return () => {
211
+ if (released) return;
212
+ released = true;
213
+ if (hostedMediaRouteRefs.get(routeKey) !== acquired) return;
214
+ acquired.count -= 1;
215
+ if (acquired.count > 0) return;
216
+ hostedMediaRouteRefs.delete(routeKey);
217
+ for (const unregisterHandle of acquired.unregisters) unregisterHandle();
218
+ };
210
219
  }
211
220
  function formatZaloError(error) {
212
221
  if (error instanceof Error) return error.stack ?? `${error.name}: ${error.message}`;
@@ -253,6 +262,7 @@ function startPollingLoop(params) {
253
262
  try {
254
263
  const response = await getUpdates(token, { timeout: pollTimeout }, fetcher);
255
264
  if (isStopped() || abortSignal.aborted) return;
265
+ if (response.ok) statusSink?.(channelReadyPatch());
256
266
  if (response.ok && response.result) {
257
267
  statusSink?.({ lastInboundAt: Date.now() });
258
268
  await processUpdate({
@@ -262,7 +272,13 @@ function startPollingLoop(params) {
262
272
  }
263
273
  } catch (err) {
264
274
  if (err instanceof ZaloApiError && err.isPollingTimeout) {} else if (!isStopped() && !abortSignal.aborted) {
265
- runtime.error?.(`[${account.accountId}] Zalo polling error: ${formatZaloError(err)}`);
275
+ const error = formatZaloError(err);
276
+ runtime.error?.(`[${account.accountId}] Zalo polling error: ${error}`);
277
+ statusSink?.({
278
+ connected: false,
279
+ lifecycle: "recovering",
280
+ lastError: error
281
+ });
266
282
  await sleepWithAbort(5e3, abortSignal).catch(() => void 0);
267
283
  }
268
284
  }
@@ -385,7 +401,7 @@ async function authorizeZaloMessage(params) {
385
401
  defaultGroupPolicy
386
402
  });
387
403
  const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
388
- const access = await resolveStableChannelMessageIngress({
404
+ const resolveChannelIngress = async (contextBinding) => await resolveStableChannelMessageIngress({
389
405
  channelId: "zalo",
390
406
  accountId: account.accountId,
391
407
  identity: {
@@ -401,6 +417,7 @@ async function authorizeZaloMessage(params) {
401
417
  kind: isGroup ? "group" : "direct",
402
418
  id: chatId
403
419
  },
420
+ contextBinding,
404
421
  providerMissingFallbackApplied,
405
422
  dmPolicy,
406
423
  groupPolicy,
@@ -409,6 +426,7 @@ async function authorizeZaloMessage(params) {
409
426
  groupAllowFrom: normalizeStringEntries(account.config.groupAllowFrom),
410
427
  command: shouldComputeAuth ? {} : void 0
411
428
  });
429
+ const access = await resolveChannelIngress();
412
430
  const senderAccess = access.senderAccess;
413
431
  if (isGroup) {
414
432
  warnMissingProviderGroupPolicyFallbackOnce({
@@ -451,6 +469,8 @@ async function authorizeZaloMessage(params) {
451
469
  return;
452
470
  }
453
471
  return {
472
+ channelIngress: access,
473
+ resolveChannelIngress,
454
474
  chatId,
455
475
  commandAuthorized: access.commandAccess.requested ? access.commandAccess.authorized : void 0,
456
476
  isGroup,
@@ -468,7 +488,7 @@ async function processMessageWithPipeline(params) {
468
488
  mediaType
469
489
  });
470
490
  if (!authorization) return;
471
- const { isGroup, chatId, senderId, senderName, rawBody, commandAuthorized } = authorization;
491
+ const { isGroup, chatId, senderId, senderName, rawBody } = authorization;
472
492
  const agentBody = agentBodyOverride ?? rawBody;
473
493
  const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
474
494
  cfg: config,
@@ -479,6 +499,17 @@ async function processMessageWithPipeline(params) {
479
499
  id: chatId
480
500
  }
481
501
  });
502
+ const channelIngress = await authorization.resolveChannelIngress({
503
+ agentId: route.agentId,
504
+ sessionKey: route.sessionKey,
505
+ messageId: message_id,
506
+ inboundEventKind: "user_request"
507
+ });
508
+ if (!channelIngress.senderAccess.allowed) {
509
+ logVerbose(core, runtime, `zalo: authorization changed before dispatch for ${senderId}`);
510
+ return;
511
+ }
512
+ const commandAuthorized = channelIngress.commandAccess.requested ? channelIngress.commandAccess.authorized : void 0;
482
513
  if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
483
514
  logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`);
484
515
  return;
@@ -492,6 +523,7 @@ async function processMessageWithPipeline(params) {
492
523
  body: agentBody
493
524
  });
494
525
  const ctxPayload = core.channel.inbound.buildContext({
526
+ channelIngress,
495
527
  channel: "zalo",
496
528
  accountId: route.accountId,
497
529
  messageId: message_id,
@@ -561,7 +593,6 @@ async function processMessageWithPipeline(params) {
561
593
  payload,
562
594
  token,
563
595
  chatId,
564
- runtime,
565
596
  core,
566
597
  config,
567
598
  webhookUrl: params.webhookUrl,
@@ -575,8 +606,8 @@ async function processMessageWithPipeline(params) {
575
606
  tableMode: "off"
576
607
  });
577
608
  },
578
- onDelivered: () => {
579
- statusSink?.({ lastOutboundAt: Date.now() });
609
+ onDelivered: (_payload, _info, result) => {
610
+ if (result?.visibleReplySent !== false) statusSink?.({ lastOutboundAt: Date.now() });
580
611
  },
581
612
  onError: (err, info) => {
582
613
  runtime.error?.(`[${account.accountId}] Zalo ${info.kind} reply failed: ${String(err)}`);
@@ -606,49 +637,64 @@ async function processMessageWithPipeline(params) {
606
637
  });
607
638
  }
608
639
  async function deliverZaloReply(params) {
609
- const { payload, token, chatId, runtime, core, config, webhookUrl, webhookPath, proxyUrl, mediaMaxBytes, canHostMedia, accountId, statusSink, fetcher } = params;
640
+ const { payload, token, chatId, core, config, webhookUrl, webhookPath, proxyUrl, mediaMaxBytes, canHostMedia, accountId, statusSink, fetcher } = params;
610
641
  const tableMode = params.tableMode ?? "code";
611
642
  const reply = resolveSendableOutboundReplyParts(payload, { text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode) });
612
643
  const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
613
- await deliverTextOrMediaReply({
614
- payload,
615
- text: reply.text,
616
- chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, ZALO_TEXT_LIMIT, chunkMode),
617
- sendText: async (chunk) => {
618
- try {
619
- await sendMessage(token, {
644
+ const acceptedMessageIds = [];
645
+ let visibleReplySent = false;
646
+ const recordAcceptedSend = (result) => {
647
+ const messageId = result.result?.message_id;
648
+ if (messageId) acceptedMessageIds.push(messageId);
649
+ visibleReplySent = true;
650
+ statusSink?.({ lastOutboundAt: Date.now() });
651
+ };
652
+ try {
653
+ await deliverTextOrMediaReply({
654
+ payload,
655
+ text: reply.text,
656
+ chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, ZALO_TEXT_LIMIT, chunkMode),
657
+ sendText: async (chunk) => {
658
+ recordAcceptedSend(await sendMessage(token, {
620
659
  chat_id: chatId,
621
660
  text: chunk
622
- }, fetcher);
623
- statusSink?.({ lastOutboundAt: Date.now() });
624
- } catch (err) {
625
- runtime.error?.(`Zalo message send failed: ${String(err)}`);
661
+ }, fetcher));
662
+ },
663
+ sendMedia: async ({ mediaUrl, caption }) => {
664
+ const sendableMediaUrl = canHostMedia && webhookUrl && webhookPath ? await prepareHostedZaloMediaUrl({
665
+ mediaUrl,
666
+ webhookUrl,
667
+ webhookPath,
668
+ maxBytes: mediaMaxBytes,
669
+ proxyUrl
670
+ }) : mediaUrl;
671
+ recordAcceptedSend(await sendPhoto(token, {
672
+ chat_id: chatId,
673
+ photo: sendableMediaUrl,
674
+ caption
675
+ }, fetcher));
626
676
  }
627
- },
628
- sendMedia: async ({ mediaUrl, caption }) => {
629
- const sendableMediaUrl = canHostMedia && webhookUrl && webhookPath ? await prepareHostedZaloMediaUrl({
630
- mediaUrl,
631
- webhookUrl,
632
- webhookPath,
633
- maxBytes: mediaMaxBytes,
634
- proxyUrl
635
- }) : mediaUrl;
636
- await sendPhoto(token, {
637
- chat_id: chatId,
638
- photo: sendableMediaUrl,
639
- caption
640
- }, fetcher);
641
- statusSink?.({ lastOutboundAt: Date.now() });
642
- },
643
- onMediaError: (error) => {
644
- runtime.error?.(`Zalo photo send failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
645
- }
646
- });
677
+ });
678
+ } catch (error) {
679
+ if (!visibleReplySent) throw error;
680
+ throw createChannelPartialDeliveryError(error, {
681
+ messageIds: acceptedMessageIds,
682
+ receipt: createMessageReceiptFromOutboundResults({
683
+ results: acceptedMessageIds.map((messageId) => ({
684
+ channel: "zalo",
685
+ messageId
686
+ })),
687
+ kind: reply.hasMedia ? "media" : "text"
688
+ }),
689
+ visibleReplySent: true
690
+ });
691
+ }
647
692
  }
648
693
  async function monitorZaloProvider(options) {
649
694
  const { token, account, config, runtime, abortSignal, useWebhook, webhookUrl, webhookSecret, webhookPath, statusSink, fetcher: fetcherOverride } = options;
650
695
  const core = getZaloRuntime();
651
- const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
696
+ const configuredMediaMaxMb = account.config.mediaMaxMb;
697
+ const effectiveMediaMaxMb = typeof configuredMediaMaxMb === "number" && configuredMediaMaxMb > 0 ? configuredMediaMaxMb : DEFAULT_MEDIA_MAX_MB;
652
698
  const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
653
699
  const mode = useWebhook ? "webhook" : "polling";
654
700
  const effectiveWebhookUrl = normalizeWebhookUrl(webhookUrl ?? account.config.webhookUrl);
@@ -674,13 +720,16 @@ async function monitorZaloProvider(options) {
674
720
  const stopOnAbort = () => {
675
721
  if (!useWebhook) stop();
676
722
  };
723
+ if (abortSignal.aborted) {
724
+ stopOnAbort();
725
+ return;
726
+ }
677
727
  abortSignal.addEventListener("abort", stopOnAbort, { once: true });
678
728
  runtime.log?.(`[${account.accountId}] Zalo provider init mode=${mode} mediaMaxMb=${String(effectiveMediaMaxMb)}`);
679
729
  try {
680
730
  if (hostedMediaRoutePath) {
681
731
  const unregisterHostedMediaRoute = registerSharedHostedMediaRoute({
682
732
  path: hostedMediaRoutePath,
683
- accountId: account.accountId,
684
733
  log: runtime.log
685
734
  });
686
735
  stopHandlers.push(unregisterHostedMediaRoute);
@@ -731,6 +780,7 @@ async function monitorZaloProvider(options) {
731
780
  source: "zalo-webhook",
732
781
  accountId: account.accountId,
733
782
  log: runtime.log,
783
+ throwOnFailure: true,
734
784
  handler: async (req, res) => {
735
785
  if (!await handleZaloWebhookRequest(req, res) && !res.headersSent) {
736
786
  res.statusCode = 404;
@@ -744,6 +794,7 @@ async function monitorZaloProvider(options) {
744
794
  url: effectiveWebhookUrl,
745
795
  secret_token: webhookSecret
746
796
  }, fetcher);
797
+ statusSink?.(channelReadyPatch());
747
798
  let webhookCleanupPromise;
748
799
  cleanupWebhook = async () => {
749
800
  if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
@@ -796,7 +847,13 @@ async function monitorZaloProvider(options) {
796
847
  });
797
848
  await waitForAbortSignal(abortSignal);
798
849
  } catch (err) {
799
- runtime.error?.(`[${account.accountId}] Zalo provider startup failed mode=${mode}: ${formatZaloError(err)}`);
850
+ const error = formatZaloError(err);
851
+ runtime.error?.(`[${account.accountId}] Zalo provider startup failed mode=${mode}: ${error}`);
852
+ statusSink?.({
853
+ connected: false,
854
+ lifecycle: "recovering",
855
+ lastError: error
856
+ });
800
857
  throw err;
801
858
  } finally {
802
859
  abortSignal.removeEventListener("abort", stopOnAbort);
@@ -1,8 +1,10 @@
1
1
  import { B as registerWebhookTargetWithPluginRoute, G as resolveWebhookTargetWithAuthOrRejectSync, V as resolveClientIp, Z as withResolvedWebhookRequestPipeline, a as WEBHOOK_ANOMALY_COUNTER_DEFAULTS, b as createFixedWindowRateLimiter, l as applyBasicWebhookRequestGuards, o as WEBHOOK_RATE_LIMIT_DEFAULTS, x as createWebhookAnomalyTracker, z as registerWebhookTarget } from "./runtime-api-qxdOyk3M.js";
2
- import { ZaloWebhookPayloadError } from "./webhook-spool-D9q-q0oh.js";
2
+ import { ZaloWebhookPayloadError } from "./webhook-spool-Bf17xwqi.js";
3
3
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
4
4
  import { readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
5
5
  //#region extensions/zalo/src/monitor.webhook.ts
6
+ const ZALO_WEBHOOK_ACCEPTED_HEADER = "x-openclaw-delivery-accepted";
7
+ const ZALO_WEBHOOK_ACCEPTED_VALUE = "durable";
6
8
  const webhookTargets = /* @__PURE__ */ new Map();
7
9
  const webhookRateLimiter = createFixedWindowRateLimiter({
8
10
  windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
@@ -18,9 +20,6 @@ function clearZaloWebhookSecurityStateForTest() {
18
20
  webhookRateLimiter.clear();
19
21
  webhookAnomalyTracker.clear();
20
22
  }
21
- function getZaloWebhookRateLimitStateSizeForTest() {
22
- return webhookRateLimiter.size();
23
- }
24
23
  function getZaloWebhookStatusCounterSizeForTest() {
25
24
  return webhookAnomalyTracker.size();
26
25
  }
@@ -108,6 +107,7 @@ async function handleZaloWebhookRequest(req, res) {
108
107
  target.runtime.error?.(`[${target.account.accountId}] Zalo webhook admission failed: ${String(error)}`);
109
108
  return true;
110
109
  }
110
+ res.setHeader(ZALO_WEBHOOK_ACCEPTED_HEADER, ZALO_WEBHOOK_ACCEPTED_VALUE);
111
111
  res.statusCode = 200;
112
112
  res.end("ok");
113
113
  return true;
@@ -116,7 +116,6 @@ async function handleZaloWebhookRequest(req, res) {
116
116
  }
117
117
  const zaloWebhookRuntime = {
118
118
  clearZaloWebhookSecurityStateForTest,
119
- getZaloWebhookRateLimitStateSizeForTest,
120
119
  getZaloWebhookStatusCounterSizeForTest,
121
120
  handleZaloWebhookRequest,
122
121
  registerZaloWebhookTarget
@@ -1,5 +1,6 @@
1
- import { d as resolveZaloAccount, f as resolveZaloToken } from "./setup-core-qcMoaIyL.js";
1
+ import { d as resolveZaloAccount, f as resolveZaloToken } from "./setup-core-T6qc2eAG.js";
2
2
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
3
+ import { stripChannelTargetPrefix, stripTargetKindPrefix } from "openclaw/plugin-sdk/core";
3
4
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
5
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
5
6
  import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
@@ -231,7 +232,7 @@ function resolveValidatedSendContext(chatId, options) {
231
232
  ok: false,
232
233
  error: "No Zalo bot token configured"
233
234
  };
234
- const trimmedChatId = chatId?.trim();
235
+ const trimmedChatId = normalizeZaloSendChatId(chatId);
235
236
  if (!trimmedChatId) return {
236
237
  ok: false,
237
238
  error: "No chat_id provided"
@@ -243,6 +244,9 @@ function resolveValidatedSendContext(chatId, options) {
243
244
  fetcher
244
245
  };
245
246
  }
247
+ function normalizeZaloSendChatId(chatId) {
248
+ return stripTargetKindPrefix(stripChannelTargetPrefix(chatId, "zalo", "zl"));
249
+ }
246
250
  function resolveSendContextOrFailure(chatId, options) {
247
251
  const context = resolveValidatedSendContext(chatId, options);
248
252
  return context.ok ? { context } : { failure: {
package/dist/setup-api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-qcMoaIyL.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-T6qc2eAG.js";
2
2
  import { n as resolveZaloRuntimeGroupPolicy } from "./group-access-8qHRzDHx.js";
3
3
  import { loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";
4
4
  //#region extensions/zalo/setup-api.ts
@@ -23,7 +23,7 @@ function createLazyObjectValue(load) {
23
23
  });
24
24
  }
25
25
  function loadSetupSurfaceModule() {
26
- return loadBundledEntryExportSync(import.meta.url, { specifier: "./src/setup-surface.js" });
26
+ return loadBundledEntryExportSync(import.meta.url, { specifier: "./setup-surface.js" });
27
27
  }
28
28
  const zaloSetupWizard = createLazyObjectValue(() => loadSetupSurfaceModule().zaloSetupWizard);
29
29
  //#endregion
@@ -3,10 +3,10 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/acco
3
3
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
5
5
  import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
6
- import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
6
+ import { buildSecretInputSchema, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
7
7
  import { createChannelDmPolicy } from "openclaw/plugin-sdk/channel-dm-policy";
8
8
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
9
- import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, createDelegatedSetupWizardProxy, createPatchedAccountSetupAdapter, createSetupInputPresenceValidator, createSetupTranslator, formatDocsLink, mergeAllowFromEntries, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/setup";
9
+ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, createDelegatedSetupWizardProxy, createPatchedAccountSetupAdapter, createSetupInputPresenceValidator, createSetupTranslator, formatDocsLink, mergeAllowFromEntries, normalizeAccountId as normalizeAccountId$1, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
10
10
  //#region \0rolldown/runtime.js
11
11
  var __defProp = Object.defineProperty;
12
12
  var __exportAll = (all, no_symbols) => {
@@ -30,58 +30,77 @@ function resolveZaloToken(config, accountId, options) {
30
30
  const accountConfig = resolveAccountEntry(baseConfig?.accounts, normalizeAccountId(resolvedAccountId));
31
31
  const accountHasBotToken = Boolean(accountConfig && Object.hasOwn(accountConfig, "botToken"));
32
32
  if (accountConfig && accountHasBotToken) {
33
- const token = options?.allowUnresolvedSecretRef ? normalizeSecretInputString(accountConfig.botToken) : normalizeResolvedSecretInputString({
33
+ const token = resolveSecretInputString({
34
34
  value: accountConfig.botToken,
35
- path: `channels.zalo.accounts.${resolvedAccountId}.botToken`
35
+ path: `channels.zalo.accounts.${resolvedAccountId}.botToken`,
36
+ mode: options?.mode
36
37
  });
37
- if (token) return {
38
- token,
39
- source: "config"
38
+ if (token.status === "available") return {
39
+ token: token.value,
40
+ source: "config",
41
+ status: "available"
42
+ };
43
+ if (token.status === "configured_unavailable") return {
44
+ token: "",
45
+ source: "config",
46
+ status: "configured_unavailable"
40
47
  };
41
48
  const fileToken = readTokenFromFile(accountConfig.tokenFile);
42
49
  if (fileToken) return {
43
50
  token: fileToken,
44
- source: "configFile"
51
+ source: "configFile",
52
+ status: "available"
45
53
  };
46
54
  }
47
55
  if (!accountHasBotToken) {
48
56
  const fileToken = readTokenFromFile(accountConfig?.tokenFile);
49
57
  if (fileToken) return {
50
58
  token: fileToken,
51
- source: "configFile"
59
+ source: "configFile",
60
+ status: "available"
52
61
  };
53
62
  }
54
63
  if (!accountHasBotToken) {
55
- const token = options?.allowUnresolvedSecretRef ? normalizeSecretInputString(baseConfig?.botToken) : normalizeResolvedSecretInputString({
64
+ const token = resolveSecretInputString({
56
65
  value: baseConfig?.botToken,
57
- path: "channels.zalo.botToken"
66
+ path: "channels.zalo.botToken",
67
+ mode: options?.mode
58
68
  });
59
- if (token) return {
60
- token,
61
- source: "config"
69
+ if (token.status === "available") return {
70
+ token: token.value,
71
+ source: "config",
72
+ status: "available"
73
+ };
74
+ if (token.status === "configured_unavailable") return {
75
+ token: "",
76
+ source: "config",
77
+ status: "configured_unavailable"
62
78
  };
63
79
  const fileToken = readTokenFromFile(baseConfig?.tokenFile);
64
80
  if (fileToken) return {
65
81
  token: fileToken,
66
- source: "configFile"
82
+ source: "configFile",
83
+ status: "available"
67
84
  };
68
85
  }
69
86
  if (isDefaultAccount) {
70
87
  const envToken = process.env.ZALO_BOT_TOKEN?.trim();
71
88
  if (envToken) return {
72
89
  token: envToken,
73
- source: "env"
90
+ source: "env",
91
+ status: "available"
74
92
  };
75
93
  }
76
94
  return {
77
95
  token: "",
78
- source: "none"
96
+ source: "none",
97
+ status: "missing"
79
98
  };
80
99
  }
81
100
  //#endregion
82
101
  //#region extensions/zalo/src/accounts.ts
83
102
  var accounts_exports = /* @__PURE__ */ __exportAll({
84
- listEnabledZaloAccounts: () => listEnabledZaloAccounts,
103
+ inspectZaloAccount: () => inspectZaloAccount,
85
104
  listZaloAccountIds: () => listZaloAccountIds,
86
105
  resolveDefaultZaloAccountId: () => resolveDefaultZaloAccountId,
87
106
  resolveZaloAccount: () => resolveZaloAccount
@@ -93,27 +112,34 @@ const { listAccountIds: listZaloAccountIds, resolveDefaultAccountId: resolveDefa
93
112
  envVars: ["ZALO_BOT_TOKEN"]
94
113
  }
95
114
  });
96
- function resolveZaloAccount(params) {
115
+ function resolveZaloAccountWithMode(params) {
97
116
  const accountId = normalizeAccountId(params.accountId ?? (params.cfg.channels?.zalo)?.defaultAccount);
98
117
  const baseEnabled = (params.cfg.channels?.zalo)?.enabled !== false;
99
118
  const merged = mergeZaloAccountConfig(params.cfg, accountId);
100
119
  const accountEnabled = merged.enabled !== false;
101
120
  const enabled = baseEnabled && accountEnabled;
102
- const tokenResolution = resolveZaloToken(params.cfg.channels?.zalo, accountId, { allowUnresolvedSecretRef: params.allowUnresolvedSecretRef });
121
+ const tokenResolution = resolveZaloToken(params.cfg.channels?.zalo, accountId, { mode: params.mode });
103
122
  return {
104
123
  accountId,
105
124
  name: normalizeOptionalString(merged.name),
106
125
  enabled,
107
126
  token: tokenResolution.token,
108
127
  tokenSource: tokenResolution.source,
128
+ tokenStatus: tokenResolution.status,
109
129
  config: merged
110
130
  };
111
131
  }
112
- function listEnabledZaloAccounts(cfg) {
113
- return listZaloAccountIds(cfg).map((accountId) => resolveZaloAccount({
114
- cfg,
115
- accountId
116
- })).filter((account) => account.enabled);
132
+ function resolveZaloAccount(params) {
133
+ return resolveZaloAccountWithMode({
134
+ ...params,
135
+ mode: "strict"
136
+ });
137
+ }
138
+ function inspectZaloAccount(params) {
139
+ return resolveZaloAccountWithMode({
140
+ ...params,
141
+ mode: "inspect"
142
+ });
117
143
  }
118
144
  //#endregion
119
145
  //#region extensions/zalo/src/setup-allow-from.ts
@@ -218,7 +244,8 @@ const zaloSetupContract = defineChannelSetupContract({
218
244
  cli: {
219
245
  flags: "--use-env",
220
246
  description: "Use ZALO_BOT_TOKEN"
221
- }
247
+ },
248
+ envVars: ["ZALO_BOT_TOKEN"]
222
249
  }
223
250
  },
224
251
  legacyAdapter: zaloSetupAdapter
@@ -232,38 +259,12 @@ const zaloDmPolicy = createChannelDmPolicy({
232
259
  accountId: accountId && normalizeAccountId$1(accountId) ? normalizeAccountId$1(accountId) ?? DEFAULT_ACCOUNT_ID$1 : resolveDefaultZaloAccountId(cfg)
233
260
  });
234
261
  },
235
- applyPatch: ({ cfg, account, patch }) => {
236
- if (account.accountId === DEFAULT_ACCOUNT_ID$1) return {
237
- ...cfg,
238
- channels: {
239
- ...cfg.channels,
240
- zalo: {
241
- ...cfg.channels?.zalo,
242
- enabled: true,
243
- ...patch
244
- }
245
- }
246
- };
247
- const currentAccount = cfg.channels?.zalo?.accounts?.[account.accountId];
248
- return {
249
- ...cfg,
250
- channels: {
251
- ...cfg.channels,
252
- zalo: {
253
- ...cfg.channels?.zalo,
254
- enabled: true,
255
- accounts: {
256
- ...cfg.channels?.zalo?.accounts,
257
- [account.accountId]: {
258
- ...currentAccount,
259
- enabled: currentAccount?.enabled ?? true,
260
- ...patch
261
- }
262
- }
263
- }
264
- }
265
- };
266
- },
262
+ applyPatch: ({ cfg, account, patch }) => patchScopedAccountConfig({
263
+ cfg,
264
+ channelKey: channel,
265
+ accountId: account.accountId,
266
+ patch
267
+ }),
267
268
  promptAllowFrom: async ({ cfg, prompter, accountId }) => promptZaloAllowFrom({
268
269
  cfg,
269
270
  prompter,
@@ -285,17 +286,8 @@ function createZaloSetupWizardProxy(loadWizard) {
285
286
  credentials: [],
286
287
  delegateFinalize: true,
287
288
  dmPolicy: zaloDmPolicy,
288
- disable: (cfg) => ({
289
- ...cfg,
290
- channels: {
291
- ...cfg.channels,
292
- zalo: {
293
- ...cfg.channels?.zalo,
294
- enabled: false
295
- }
296
- }
297
- })
289
+ disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
298
290
  });
299
291
  }
300
292
  //#endregion
301
- export { noteZaloTokenHelp as a, listEnabledZaloAccounts as c, resolveZaloAccount as d, resolveZaloToken as f, zaloSetupContract as i, listZaloAccountIds as l, normalizeSecretInputString as m, zaloDmPolicy as n, promptZaloAllowFrom as o, buildSecretInputSchema as p, zaloSetupAdapter as r, accounts_exports as s, createZaloSetupWizardProxy as t, resolveDefaultZaloAccountId as u };
293
+ export { noteZaloTokenHelp as a, inspectZaloAccount as c, resolveZaloAccount as d, resolveZaloToken as f, __exportAll as h, zaloSetupContract as i, listZaloAccountIds as l, normalizeSecretInputString as m, zaloDmPolicy as n, promptZaloAllowFrom as o, buildSecretInputSchema as p, zaloSetupAdapter as r, accounts_exports as s, createZaloSetupWizardProxy as t, resolveDefaultZaloAccountId as u };
@@ -1,6 +1,7 @@
1
- import { a as noteZaloTokenHelp, d as resolveZaloAccount, n as zaloDmPolicy, o as promptZaloAllowFrom } from "./setup-core-qcMoaIyL.js";
1
+ import { a as noteZaloTokenHelp, c as inspectZaloAccount, h as __exportAll, n as zaloDmPolicy, o as promptZaloAllowFrom } from "./setup-core-T6qc2eAG.js";
2
2
  import { DEFAULT_ACCOUNT_ID, buildSingleChannelSecretPromptState, createSetupTranslator, createStandardChannelSetupStatus, hasConfiguredSecretInput, promptSingleChannelSecretInput, runSingleChannelSecretStep } from "openclaw/plugin-sdk/setup";
3
3
  //#region extensions/zalo/src/setup-surface.ts
4
+ var setup_surface_exports = /* @__PURE__ */ __exportAll({ zaloSetupWizard: () => zaloSetupWizard });
4
5
  const t = createSetupTranslator();
5
6
  const channel = "zalo";
6
7
  function setZaloUpdateMode(cfg, accountId, mode, webhookUrl, webhookSecret, webhookPath) {
@@ -72,10 +73,9 @@ const zaloSetupWizard = {
72
73
  unconfiguredScore: 10,
73
74
  includeStatusLine: true,
74
75
  resolveConfigured: ({ cfg, accountId }) => {
75
- const account = resolveZaloAccount({
76
+ const account = inspectZaloAccount({
76
77
  cfg,
77
- accountId,
78
- allowUnresolvedSecretRef: true
78
+ accountId
79
79
  });
80
80
  return Boolean(account.token) || hasConfiguredSecretInput(account.config.botToken) || Boolean(account.config.tokenFile?.trim());
81
81
  }
@@ -83,10 +83,9 @@ const zaloSetupWizard = {
83
83
  credentials: [],
84
84
  finalize: async ({ cfg, accountId, forceAllowFrom, options, prompter }) => {
85
85
  let next = cfg;
86
- const resolvedAccount = resolveZaloAccount({
86
+ const resolvedAccount = inspectZaloAccount({
87
87
  cfg: next,
88
- accountId,
89
- allowUnresolvedSecretRef: true
88
+ accountId
90
89
  });
91
90
  const accountConfigured = Boolean(resolvedAccount.token);
92
91
  const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
@@ -213,4 +212,4 @@ const zaloSetupWizard = {
213
212
  dmPolicy: zaloDmPolicy
214
213
  };
215
214
  //#endregion
216
- export { zaloSetupWizard };
215
+ export { zaloSetupWizard as n, setup_surface_exports as t };
@@ -0,0 +1,3 @@
1
+ import { r as zaloSetupAdapter } from "./setup-core-T6qc2eAG.js";
2
+ import { n as zaloSetupWizard } from "./setup-surface-edfRk3eu.js";
3
+ export { zaloSetupAdapter, zaloSetupWizard };
@@ -1,28 +1,15 @@
1
1
  import { t as getZaloRuntime } from "./runtime-api-qxdOyk3M.js";
2
- import { r as ZaloApiError } from "./send-BeG5dH7I.js";
3
- import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
2
+ import { r as ZaloApiError } from "./send-BGGtv8x_.js";
3
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
4
+ import { normalizeNullableString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
6
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
7
  import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
5
8
  //#region extensions/zalo/src/webhook-spool.ts
6
9
  const ZALO_WEBHOOK_SPOOL_VERSION = 1;
7
10
  const ZALO_WEBHOOK_DRAIN_INTERVAL_MS = 500;
8
11
  const ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
9
- const ZALO_WEBHOOK_PRUNE_INTERVAL_MS = 3600 * 1e3;
10
- const ZALO_WEBHOOK_COMPLETED_TTL_MS = 720 * 60 * 6e4;
11
- const ZALO_WEBHOOK_COMPLETED_MAX_ENTRIES = 2e4;
12
- const ZALO_WEBHOOK_FAILED_TTL_MS = 720 * 60 * 6e4;
13
- const ZALO_WEBHOOK_FAILED_MAX_ENTRIES = 5e3;
14
- var ZaloWebhookPayloadError = class extends Error {
15
- constructor(message, options) {
16
- super(message, options);
17
- this.name = "ZaloWebhookPayloadError";
18
- }
19
- };
20
- function isRecord(value) {
21
- return typeof value === "object" && value !== null && !Array.isArray(value);
22
- }
23
- function nonEmptyString(value) {
24
- return typeof value === "string" && value.trim() ? value.trim() : null;
25
- }
12
+ const ZaloWebhookPayloadError = createChannelIngressError("ZaloWebhookPayloadError");
26
13
  function parseRawRecord(rawEvent) {
27
14
  let parsed;
28
15
  try {
@@ -40,9 +27,9 @@ function resolveUpdateRecord(envelope) {
40
27
  function inspectZaloWebhookEvent(rawEvent) {
41
28
  const update = resolveUpdateRecord(parseRawRecord(rawEvent));
42
29
  const message = isRecord(update.message) ? update.message : null;
43
- const eventId = nonEmptyString(message?.message_id);
30
+ const eventId = normalizeNullableString(message?.message_id);
44
31
  if (!eventId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.message_id.");
45
- const chatId = nonEmptyString((isRecord(message?.chat) ? message.chat : null)?.id);
32
+ const chatId = normalizeNullableString((isRecord(message?.chat) ? message.chat : null)?.id);
46
33
  if (!chatId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.chat.id.");
47
34
  return {
48
35
  eventId,
@@ -54,20 +41,17 @@ function parseClaimedUpdate(payload, claimedId) {
54
41
  if (payload.version !== ZALO_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string") throw new ZaloWebhookPayloadError("Zalo webhook spool payload is invalid.");
55
42
  const facts = inspectZaloWebhookEvent(payload.rawEvent);
56
43
  if (facts.eventId !== claimedId) throw new ZaloWebhookPayloadError("Zalo webhook message id changed after durable admission.");
57
- const eventName = nonEmptyString(facts.update.event_name);
44
+ const eventName = normalizeNullableString(facts.update.event_name);
58
45
  if (eventName !== "message.text.received" && eventName !== "message.image.received" && eventName !== "message.sticker.received" && eventName !== "message.unsupported.received") throw new ZaloWebhookPayloadError("Zalo webhook event_name is unsupported.");
59
46
  const message = facts.update.message;
60
47
  const from = isRecord(message.from) ? message.from : null;
61
48
  const chat = isRecord(message.chat) ? message.chat : null;
62
- if (!nonEmptyString(from?.id)) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.from.id.");
49
+ if (!normalizeNullableString(from?.id)) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.from.id.");
63
50
  if (chat?.chat_type !== "PRIVATE" && chat?.chat_type !== "GROUP") throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid chat type.");
64
51
  if (typeof message.date !== "number" || !Number.isFinite(message.date)) throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid date.");
65
52
  if (eventName === "message.text.received" && typeof message.text !== "string") throw new ZaloWebhookPayloadError("Zalo text event is missing message.text.");
66
53
  return facts.update;
67
54
  }
68
- function errorText(error) {
69
- return error instanceof Error ? error.message : String(error);
70
- }
71
55
  function isZaloAuthenticationFailure(error) {
72
56
  let current = error;
73
57
  const seen = /* @__PURE__ */ new Set();
@@ -80,10 +64,8 @@ function isZaloAuthenticationFailure(error) {
80
64
  return false;
81
65
  }
82
66
  function createZaloWebhookIngress(options) {
83
- const queue = options.queue ?? getZaloRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
84
- const deferredClaims = /* @__PURE__ */ new Map();
85
67
  const monitor = createChannelIngressMonitor({
86
- queue,
68
+ queue: options.queue ?? getZaloRuntime().state.openChannelIngressQueue({ accountId: options.accountId }),
87
69
  inspect: (rawEvent) => inspectZaloWebhookEvent(rawEvent),
88
70
  payload: {
89
71
  storage: "raw-event",
@@ -94,47 +76,13 @@ function createZaloWebhookIngress(options) {
94
76
  },
95
77
  deliver: async (_rawEvent, lifecycle, claim) => {
96
78
  const update = parseClaimedUpdate(claim.payload, claim.id);
97
- const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
98
- let resolveDeferredClaim;
99
- const deferredClaim = new Promise((resolve) => {
100
- resolveDeferredClaim = resolve;
101
- });
102
- let deferredClaimSettled = false;
103
- const settleDeferredClaim = () => {
104
- if (deferredClaimSettled) return;
105
- deferredClaimSettled = true;
106
- if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
107
- resolveDeferredClaim();
108
- };
109
- await options.deliver(update, {
110
- ...boundLifecycle,
111
- onAdopted: async () => {
112
- try {
113
- await boundLifecycle.onAdopted();
114
- } finally {
115
- settleDeferredClaim();
116
- }
117
- },
118
- onDeferred: () => {
119
- if (!deferredClaimSettled) deferredClaims.set(claim.id, deferredClaim);
120
- boundLifecycle.onDeferred();
121
- },
122
- onAbandoned: () => {
123
- Promise.resolve(boundLifecycle.onAbandoned()).finally(settleDeferredClaim);
124
- }
125
- });
126
- return deferredClaims.has(claim.id) ? { kind: "deferred" } : { kind: "completed" };
79
+ await options.deliver(update, bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle);
127
80
  },
128
81
  pollIntervalMs: ZALO_WEBHOOK_DRAIN_INTERVAL_MS,
129
- retention: {
130
- pruneIntervalMs: ZALO_WEBHOOK_PRUNE_INTERVAL_MS,
131
- completedTtlMs: ZALO_WEBHOOK_COMPLETED_TTL_MS,
132
- completedMaxEntries: ZALO_WEBHOOK_COMPLETED_MAX_ENTRIES,
133
- failedTtlMs: ZALO_WEBHOOK_FAILED_TTL_MS,
134
- failedMaxEntries: ZALO_WEBHOOK_FAILED_MAX_ENTRIES
135
- },
82
+ retention: { failedMaxEntries: 5e3 },
136
83
  waitForDeliveryIdleBeforeRepump: false,
137
84
  runPumpTask: runDetachedWebhookWork,
85
+ deferredClaims: "wait-on-stop",
138
86
  drain: {
139
87
  adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
140
88
  startLimit: ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
@@ -149,24 +97,21 @@ function createZaloWebhookIngress(options) {
149
97
  };
150
98
  if (isZaloAuthenticationFailure(error)) return {
151
99
  reason: "authentication-failed",
152
- message: errorText(error)
100
+ message: formatErrorMessage(error)
153
101
  };
154
102
  return null;
155
103
  },
156
104
  onLog: (message) => options.runtime.error?.(`zalo ingress: ${message}`)
157
105
  },
158
106
  createStoppedError: () => /* @__PURE__ */ new Error("Zalo ingress stopped."),
159
- onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${errorText(error)}`)
107
+ onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${formatErrorMessage(error)}`)
160
108
  });
161
109
  return {
162
110
  accept: async (rawEvent) => {
163
111
  await monitor.admit(rawEvent);
164
112
  },
165
113
  start: monitor.start,
166
- stop: async () => {
167
- await monitor.stop();
168
- await Promise.allSettled(deferredClaims.values());
169
- }
114
+ stop: monitor.stop
170
115
  };
171
116
  }
172
117
  const zaloWebhookIngressRuntime = { createZaloWebhookIngress };
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "id": "zalo",
3
+ "doctorContract": {},
3
4
  "name": "Zalo",
4
5
  "description": "OpenClaw Zalo channel plugin for bot and webhook chats.",
5
6
  "icon": "https://cdn.simpleicons.org/zalo",
@@ -26,6 +27,9 @@
26
27
  "enabled": {
27
28
  "type": "boolean"
28
29
  },
30
+ "configWrites": {
31
+ "type": "boolean"
32
+ },
29
33
  "markdown": {
30
34
  "type": "object",
31
35
  "properties": {
@@ -71,6 +75,29 @@
71
75
  ],
72
76
  "additionalProperties": false
73
77
  },
78
+ {
79
+ "type": "object",
80
+ "properties": {
81
+ "source": {
82
+ "type": "string",
83
+ "const": "store"
84
+ },
85
+ "provider": {
86
+ "type": "string",
87
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
88
+ },
89
+ "id": {
90
+ "type": "string",
91
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
92
+ }
93
+ },
94
+ "required": [
95
+ "source",
96
+ "provider",
97
+ "id"
98
+ ],
99
+ "additionalProperties": false
100
+ },
74
101
  {
75
102
  "type": "object",
76
103
  "properties": {
@@ -155,6 +182,29 @@
155
182
  ],
156
183
  "additionalProperties": false
157
184
  },
185
+ {
186
+ "type": "object",
187
+ "properties": {
188
+ "source": {
189
+ "type": "string",
190
+ "const": "store"
191
+ },
192
+ "provider": {
193
+ "type": "string",
194
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
195
+ },
196
+ "id": {
197
+ "type": "string",
198
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
199
+ }
200
+ },
201
+ "required": [
202
+ "source",
203
+ "provider",
204
+ "id"
205
+ ],
206
+ "additionalProperties": false
207
+ },
158
208
  {
159
209
  "type": "object",
160
210
  "properties": {
@@ -270,6 +320,9 @@
270
320
  "enabled": {
271
321
  "type": "boolean"
272
322
  },
323
+ "configWrites": {
324
+ "type": "boolean"
325
+ },
273
326
  "markdown": {
274
327
  "type": "object",
275
328
  "properties": {
@@ -315,6 +368,29 @@
315
368
  ],
316
369
  "additionalProperties": false
317
370
  },
371
+ {
372
+ "type": "object",
373
+ "properties": {
374
+ "source": {
375
+ "type": "string",
376
+ "const": "store"
377
+ },
378
+ "provider": {
379
+ "type": "string",
380
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
381
+ },
382
+ "id": {
383
+ "type": "string",
384
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
385
+ }
386
+ },
387
+ "required": [
388
+ "source",
389
+ "provider",
390
+ "id"
391
+ ],
392
+ "additionalProperties": false
393
+ },
318
394
  {
319
395
  "type": "object",
320
396
  "properties": {
@@ -399,6 +475,29 @@
399
475
  ],
400
476
  "additionalProperties": false
401
477
  },
478
+ {
479
+ "type": "object",
480
+ "properties": {
481
+ "source": {
482
+ "type": "string",
483
+ "const": "store"
484
+ },
485
+ "provider": {
486
+ "type": "string",
487
+ "pattern": "^[a-z][a-z0-9_-]{0,63}$"
488
+ },
489
+ "id": {
490
+ "type": "string",
491
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
492
+ }
493
+ },
494
+ "required": [
495
+ "source",
496
+ "provider",
497
+ "id"
498
+ ],
499
+ "additionalProperties": false
500
+ },
402
501
  {
403
502
  "type": "object",
404
503
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/zalo",
3
- "version": "2026.7.2-beta.5",
3
+ "version": "2026.8.1-beta.2",
4
4
  "description": "OpenClaw Zalo channel plugin for bot and webhook chats.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "peerDependencies": {
11
- "openclaw": ">=2026.7.2-beta.5"
11
+ "openclaw": ">=2026.8.1-beta.2"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -69,7 +69,10 @@
69
69
  "cli": {
70
70
  "flags": "--use-env",
71
71
  "description": "Use ZALO_BOT_TOKEN"
72
- }
72
+ },
73
+ "envVars": [
74
+ "ZALO_BOT_TOKEN"
75
+ ]
73
76
  }
74
77
  ]
75
78
  }
@@ -80,10 +83,10 @@
80
83
  "minHostVersion": ">=2026.4.10"
81
84
  },
82
85
  "compat": {
83
- "pluginApi": ">=2026.7.2-beta.5"
86
+ "pluginApi": ">=2026.8.1-beta.2"
84
87
  },
85
88
  "build": {
86
- "openclawVersion": "2026.7.2-beta.5"
89
+ "openclawVersion": "2026.8.1-beta.2"
87
90
  },
88
91
  "release": {
89
92
  "publishToClawHub": true,