@openclaw/zalo 2026.7.2-beta.2 → 2026.7.2-beta.4

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-9sVVxyap.js";
1
+ import { t as sendMessageZalo } from "./send-WPSy5EgF.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-fwCh0QUi.js";
2
- import { t as zaloPlugin } from "./channel-CBPOM3Pc.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-BBA-V-j0.js";
2
+ import { t as zaloPlugin } from "./channel-CvZItLo-.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 listZaloAccountIds, f as buildSecretInputSchema, l as resolveDefaultZaloAccountId, r as zaloSetupAdapter, s as listEnabledZaloAccounts, t as createZaloSetupWizardProxy, u as resolveZaloAccount } from "./setup-core-fwCh0QUi.js";
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-BBA-V-j0.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";
@@ -14,7 +14,7 @@ import { createChannelDirectoryAdapter, listResolvedDirectoryUserEntriesFromAllo
14
14
  import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
15
15
  import { sendPayloadWithChunkedTextAndMedia } from "openclaw/plugin-sdk/reply-payload";
16
16
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, standardDmPolicyOpenIssue } from "openclaw/plugin-sdk/status-helpers";
17
- import { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
17
+ import { chunkTextForOutbound, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
18
18
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
19
19
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix, stripTargetKindPrefix } from "openclaw/plugin-sdk/core";
20
20
  import { jsonResult, readStringParam } from "openclaw/plugin-sdk/channel-actions";
@@ -24,7 +24,7 @@ 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-OmfJFYZn.js"), "zaloActionsRuntime");
27
+ const loadZaloActionsRuntime = createLazyRuntimeNamedExport(() => import("./actions.runtime-DwjVG1H3.js"), "zaloActionsRuntime");
28
28
  const providerId = "zalo";
29
29
  const ZALO_ACTIONS = /* @__PURE__ */ new Set(["send"]);
30
30
  function listEnabledAccounts(cfg, accountId) {
@@ -176,8 +176,8 @@ function looksLikeZaloChatId(raw, normalized) {
176
176
  const target = normalizeZaloMessagingTarget(normalized ?? raw);
177
177
  return Boolean(target);
178
178
  }
179
- const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-BsZBukDW.js"));
180
- const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-8eRimod9.js")).zaloSetupWizard);
179
+ const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-U7gDleS5.js"));
180
+ const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-dFkadq_c.js")).zaloSetupWizard);
181
181
  const zaloTextChunkLimit = 2e3;
182
182
  async function sendZaloDelivery(ctx) {
183
183
  const result = await (await loadZaloChannelRuntime()).sendZaloText({
@@ -258,6 +258,7 @@ const zaloPlugin = createChatChannelPlugin({
258
258
  id: "zalo",
259
259
  meta,
260
260
  setup: zaloSetupAdapter,
261
+ setupContract: zaloSetupContract,
261
262
  setupWizard: zaloSetupWizard,
262
263
  capabilities: {
263
264
  chatTypes: ["direct", "group"],
@@ -347,6 +348,7 @@ const zaloPlugin = createChatChannelPlugin({
347
348
  chunker: chunkTextForOutbound,
348
349
  chunkerMode: "text",
349
350
  textChunkLimit: zaloTextChunkLimit,
351
+ sanitizeText: ({ text }) => sanitizeAssistantVisibleText(text),
350
352
  sendPayload: async (ctx) => await sendPayloadWithChunkedTextAndMedia({
351
353
  ctx,
352
354
  textChunkLimit: zaloTextChunkLimit,
@@ -1,2 +1,2 @@
1
- import { t as zaloPlugin } from "./channel-CBPOM3Pc.js";
1
+ import { t as zaloPlugin } from "./channel-CvZItLo-.js";
2
2
  export { zaloPlugin };
@@ -1,6 +1,6 @@
1
- import { p as normalizeSecretInputString } from "./setup-core-fwCh0QUi.js";
2
- import { i as PAIRING_APPROVED_MESSAGE } from "./runtime-api-CAA4ZtqF.js";
3
- import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-9sVVxyap.js";
1
+ import { m as normalizeSecretInputString } from "./setup-core-BBA-V-j0.js";
2
+ import { i as PAIRING_APPROVED_MESSAGE } from "./runtime-api-B53PGWBW.js";
3
+ import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-WPSy5EgF.js";
4
4
  import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-outbound";
5
5
  //#region extensions/zalo/src/probe.ts
6
6
  async function probeZalo(token, timeoutMs = 5e3, fetcher) {
@@ -52,7 +52,7 @@ async function probeZalo(token, timeoutMs = 5e3, fetcher) {
52
52
  //#endregion
53
53
  //#region extensions/zalo/src/channel.runtime.ts
54
54
  async function notifyZaloPairingApproval(params) {
55
- const { resolveZaloAccount } = await import("./setup-core-fwCh0QUi.js").then((n) => n.o);
55
+ const { resolveZaloAccount } = await import("./setup-core-BBA-V-j0.js").then((n) => n.s);
56
56
  const account = resolveZaloAccount({ cfg: params.cfg });
57
57
  if (!account.token) throw new Error("Zalo token not configured");
58
58
  await sendMessageZalo(params.id, PAIRING_APPROVED_MESSAGE, { token: account.token });
@@ -86,7 +86,7 @@ async function startZaloGatewayAccount(ctx) {
86
86
  setStatus: ctx.setStatus
87
87
  });
88
88
  ctx.log?.info(`[${account.accountId}] starting provider${zaloBotLabel} mode=${mode}`);
89
- const { monitorZaloProvider } = await import("./monitor-DTi243P3.js");
89
+ const { monitorZaloProvider } = await import("./monitor-DfHhlqTv.js");
90
90
  return monitorZaloProvider({
91
91
  token,
92
92
  account,
@@ -1,16 +1,15 @@
1
1
  import { n as resolveZaloRuntimeGroupPolicy, t as normalizeZaloAllowEntry } from "./group-access-8qHRzDHx.js";
2
- import { t as getZaloRuntime } from "./runtime-api-CAA4ZtqF.js";
3
- import { c as sendChatAction, d as setWebhook, i as deleteWebhook, l as sendMessage, n as resolveZaloProxyFetch, o as getUpdates, r as ZaloApiError, s as getWebhookInfo, u as sendPhoto } from "./send-9sVVxyap.js";
4
- import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
2
+ import { t as getZaloRuntime } from "./runtime-api-B53PGWBW.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-WPSy5EgF.js";
4
+ import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
5
5
  import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
6
6
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import { resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
8
8
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
9
9
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
10
- import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
11
10
  import { registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
12
11
  import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
13
- import { formatInboundMediaUnavailableText } from "openclaw/plugin-sdk/channel-inbound";
12
+ import { formatInboundMediaUnavailableText, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
14
13
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
15
14
  import { sleepWithAbort, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
16
15
  import { createHostedOutboundMediaStore } from "openclaw/plugin-sdk/outbound-media";
@@ -31,7 +30,6 @@ function resolveZaloDurableReplyOptions(params) {
31
30
  }
32
31
  //#endregion
33
32
  //#region extensions/zalo/src/outbound-media.ts
34
- const ZALO_OUTBOUND_MEDIA_TTL_MS = 2 * 6e4;
35
33
  const ZALO_OUTBOUND_MEDIA_SEGMENT = "media";
36
34
  const ZALO_OUTBOUND_MEDIA_PREFIX = `/${ZALO_OUTBOUND_MEDIA_SEGMENT}/`;
37
35
  const ZALO_OUTBOUND_MEDIA_ID_RE = /^[a-f0-9]{24}$/;
@@ -75,7 +73,7 @@ function resolveHostedZaloMediaRoutePath(params) {
75
73
  }
76
74
  async function prepareHostedZaloMediaUrl(params) {
77
75
  const now = asDateTimestampMs(Date.now());
78
- if ((now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(ZALO_OUTBOUND_MEDIA_TTL_MS, { nowMs: now })) === void 0) throw new Error("Zalo outbound media expiry could not be resolved");
76
+ if ((now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(12e4, { nowMs: now })) === void 0) throw new Error("Zalo outbound media expiry could not be resolved");
79
77
  const routePath = resolveHostedZaloMediaRoutePath({
80
78
  webhookUrl: params.webhookUrl,
81
79
  webhookPath: params.webhookPath
@@ -165,7 +163,12 @@ function resolveZaloTimestampMs(date) {
165
163
  if (!date) return;
166
164
  return date >= UNIX_MILLISECONDS_THRESHOLD ? date : date * 1e3;
167
165
  }
168
- const loadZaloWebhookModule = createLazyRuntimeModule(() => import("./monitor.webhook-BFkM-Syx.js"));
166
+ const loadZaloWebhookRuntime = createLazyRuntimeNamedExport(() => import("./monitor.webhook-BCTmWQY8.js"), "zaloWebhookRuntime");
167
+ const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-Ozf0tcbs.js"), "zaloWebhookIngressRuntime");
168
+ const loadZaloWebhookModule = createLazyRuntimeModule(async () => ({
169
+ ...await loadZaloWebhookRuntime(),
170
+ ...await loadZaloWebhookIngressRuntime()
171
+ }));
169
172
  function releaseSharedHostedMediaRouteRef(routePath) {
170
173
  const current = hostedMediaRouteRefs.get(routePath);
171
174
  if (!current) return;
@@ -226,22 +229,7 @@ function logVerbose(core, runtime, message) {
226
229
  }
227
230
  async function handleZaloWebhookRequest(req, res) {
228
231
  const { handleZaloWebhookRequest: handleZaloWebhookRequestInternal } = await loadZaloWebhookModule();
229
- return await handleZaloWebhookRequestInternal(req, res, async ({ update, target }) => {
230
- await processUpdate({
231
- update,
232
- token: target.token,
233
- account: target.account,
234
- config: target.config,
235
- runtime: target.runtime,
236
- core: target.core,
237
- mediaMaxMb: target.mediaMaxMb,
238
- canHostMedia: target.canHostMedia,
239
- webhookUrl: target.webhookUrl,
240
- webhookPath: target.webhookPath,
241
- statusSink: target.statusSink,
242
- fetcher: target.fetcher
243
- });
244
- });
232
+ return await handleZaloWebhookRequestInternal(req, res);
245
233
  }
246
234
  function startPollingLoop(params) {
247
235
  const { token, account, config, runtime, core, mediaMaxMb, canHostMedia, webhookUrl, webhookPath, abortSignal, isStopped, statusSink, fetcher } = params;
@@ -298,7 +286,8 @@ async function processUpdate(params) {
298
286
  webhookUrl: params.webhookUrl,
299
287
  webhookPath: params.webhookPath,
300
288
  statusSink,
301
- fetcher
289
+ fetcher,
290
+ turnAdoptionLifecycle: params.turnAdoptionLifecycle
302
291
  };
303
292
  if (!message) return;
304
293
  switch (event_name) {
@@ -330,6 +319,7 @@ async function handleTextMessage(params) {
330
319
  await processMessageWithPipeline({
331
320
  ...params,
332
321
  text,
322
+ mediaKind: void 0,
333
323
  mediaPath: void 0,
334
324
  mediaType: void 0
335
325
  });
@@ -340,7 +330,8 @@ async function handleImageMessage(params) {
340
330
  const authorization = await authorizeZaloMessage({
341
331
  ...params,
342
332
  text: caption,
343
- mediaPath: "__pending_media__",
333
+ mediaKind: "image",
334
+ mediaPath: void 0,
344
335
  mediaType: void 0
345
336
  });
346
337
  if (!authorization) return;
@@ -360,21 +351,21 @@ async function handleImageMessage(params) {
360
351
  runtime.error?.(`[${account.accountId}] Failed to download Zalo image: ${String(err)}`);
361
352
  }
362
353
  const agentBody = mediaPath ? authorization.rawBody : formatInboundMediaUnavailableText({
363
- body: caption,
364
- mediaPlaceholder: "<media:image>",
354
+ body: authorization.rawBody,
365
355
  notice: "[zalo image attachment unavailable]"
366
356
  });
367
357
  await processMessageWithPipeline({
368
358
  ...params,
369
359
  authorization,
370
360
  agentBody,
361
+ mediaKind: "image",
371
362
  text: caption,
372
363
  mediaPath,
373
364
  mediaType
374
365
  });
375
366
  }
376
367
  async function authorizeZaloMessage(params) {
377
- const { message, account, config, runtime, core, text, mediaPath, token, statusSink, fetcher } = params;
368
+ const { message, account, config, runtime, core, text, token, statusSink, fetcher } = params;
378
369
  const pairing = createChannelPairingController({
379
370
  core,
380
371
  channel: "zalo",
@@ -387,7 +378,7 @@ async function authorizeZaloMessage(params) {
387
378
  const senderName = from.display_name ?? from.name;
388
379
  const dmPolicy = account.config.dmPolicy ?? "pairing";
389
380
  const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
390
- const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
381
+ const rawBody = text?.trim() ?? "";
391
382
  const { groupPolicy, providerMissingFallbackApplied } = resolveZaloRuntimeGroupPolicy({
392
383
  providerConfigPresent: config.channels?.zalo !== void 0,
393
384
  groupPolicy: account.config.groupPolicy,
@@ -469,7 +460,7 @@ async function authorizeZaloMessage(params) {
469
460
  };
470
461
  }
471
462
  async function processMessageWithPipeline(params) {
472
- const { message, token, account, config, runtime, core, mediaPath, mediaType, statusSink, fetcher, agentBody: agentBodyOverride, authorization: authorizationOverride } = params;
463
+ const { message, token, account, config, runtime, core, mediaPath, mediaKind, mediaType, statusSink, fetcher, agentBody: agentBodyOverride, authorization: authorizationOverride } = params;
473
464
  const { message_id, date } = message;
474
465
  const authorization = authorizationOverride ?? await authorizeZaloMessage({
475
466
  ...params,
@@ -479,16 +470,14 @@ async function processMessageWithPipeline(params) {
479
470
  if (!authorization) return;
480
471
  const { isGroup, chatId, senderId, senderName, rawBody, commandAuthorized } = authorization;
481
472
  const agentBody = agentBodyOverride ?? rawBody;
482
- const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
473
+ const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
483
474
  cfg: config,
484
475
  channel: "zalo",
485
476
  accountId: account.accountId,
486
477
  peer: {
487
478
  kind: isGroup ? "group" : "direct",
488
479
  id: chatId
489
- },
490
- runtime: core.channel,
491
- sessionStore: config.session?.store
480
+ }
492
481
  });
493
482
  if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
494
483
  logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`);
@@ -496,7 +485,7 @@ async function processMessageWithPipeline(params) {
496
485
  }
497
486
  const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
498
487
  const timestamp = resolveZaloTimestampMs(date);
499
- const { storePath, body } = buildEnvelope({
488
+ const body = buildEnvelope({
500
489
  channel: "Zalo",
501
490
  from: fromLabel,
502
491
  timestamp,
@@ -519,6 +508,7 @@ async function processMessageWithPipeline(params) {
519
508
  },
520
509
  route: {
521
510
  agentId: route.agentId,
511
+ dmScope: route.dmScope,
522
512
  accountId: route.accountId,
523
513
  routeSessionKey: route.sessionKey
524
514
  },
@@ -529,10 +519,11 @@ async function processMessageWithPipeline(params) {
529
519
  rawBody,
530
520
  commandBody: rawBody
531
521
  },
532
- media: mediaPath || mediaType ? [{
522
+ media: mediaKind || mediaPath || mediaType ? [{
533
523
  path: mediaPath,
534
524
  url: mediaPath,
535
- contentType: mediaType
525
+ contentType: mediaType,
526
+ kind: mediaKind ?? void 0
536
527
  }] : void 0,
537
528
  extra: {
538
529
  CommandAuthorized: commandAuthorized,
@@ -544,16 +535,16 @@ async function processMessageWithPipeline(params) {
544
535
  channel: "zalo",
545
536
  accountId: account.accountId
546
537
  });
547
- await core.channel.inbound.dispatchReply({
538
+ await core.channel.inbound.dispatch({
548
539
  cfg: config,
549
540
  channel: "zalo",
550
541
  accountId: account.accountId,
551
- agentId: route.agentId,
552
- routeSessionKey: route.sessionKey,
553
- storePath,
542
+ route: {
543
+ agentId: route.agentId,
544
+ dmScope: route.dmScope,
545
+ sessionKey: route.sessionKey
546
+ },
554
547
  ctxPayload,
555
- recordInboundSession: core.channel.session.recordInboundSession,
556
- dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
557
548
  delivery: {
558
549
  preparePayload: (payload) => prepareZaloDurableReplyPayload({
559
550
  payload,
@@ -610,7 +601,8 @@ async function processMessageWithPipeline(params) {
610
601
  } },
611
602
  record: { onRecordError: (err) => {
612
603
  runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
613
- } }
604
+ } },
605
+ ...params.turnAdoptionLifecycle ? { turnAdoptionLifecycle: params.turnAdoptionLifecycle } : {}
614
606
  });
615
607
  }
616
608
  async function deliverZaloReply(params) {
@@ -672,6 +664,7 @@ async function monitorZaloProvider(options) {
672
664
  }) : void 0;
673
665
  let stopped = false;
674
666
  const stopHandlers = [];
667
+ const asyncStopHandlers = [];
675
668
  let cleanupWebhook;
676
669
  const stop = () => {
677
670
  if (stopped) return;
@@ -693,46 +686,44 @@ async function monitorZaloProvider(options) {
693
686
  stopHandlers.push(unregisterHostedMediaRoute);
694
687
  }
695
688
  if (useWebhook) {
696
- const { registerZaloWebhookTarget } = await loadZaloWebhookModule();
689
+ const { createZaloWebhookIngress, registerZaloWebhookTarget } = await loadZaloWebhookModule();
697
690
  if (!effectiveWebhookUrl || !webhookSecret) throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
698
691
  if (!effectiveWebhookUrl.startsWith("https://")) throw new Error("Zalo webhook URL must use HTTPS");
699
692
  if (webhookSecret.length < 8 || webhookSecret.length > 256) throw new Error("Zalo webhook secret must be 8-256 characters");
700
693
  const path = effectiveWebhookPath;
701
694
  if (!path) throw new Error("Zalo webhookPath could not be derived");
702
695
  runtime.log?.(`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(effectiveWebhookUrl)}`);
703
- await setWebhook(token, {
704
- url: effectiveWebhookUrl,
705
- secret_token: webhookSecret
706
- }, fetcher);
707
- let webhookCleanupPromise;
708
- cleanupWebhook = async () => {
709
- if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
710
- runtime.log?.(`[${account.accountId}] Zalo stopping; deleting webhook`);
711
- try {
712
- await deleteWebhook(token, fetcher, WEBHOOK_CLEANUP_TIMEOUT_MS);
713
- runtime.log?.(`[${account.accountId}] Zalo webhook deleted`);
714
- } catch (err) {
715
- const detail = err instanceof Error && err.name === "AbortError" ? `timed out after ${String(WEBHOOK_CLEANUP_TIMEOUT_MS)}ms` : formatZaloError(err);
716
- runtime.error?.(`[${account.accountId}] Zalo webhook delete failed: ${detail}`);
717
- }
718
- })();
719
- await webhookCleanupPromise;
720
- };
721
- runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
696
+ const ingress = createZaloWebhookIngress({
697
+ accountId: account.accountId,
698
+ runtime,
699
+ deliver: async (update, turnAdoptionLifecycle) => {
700
+ statusSink?.({ lastInboundAt: Date.now() });
701
+ await processUpdate({
702
+ update,
703
+ token,
704
+ account,
705
+ config,
706
+ runtime,
707
+ core,
708
+ mediaMaxMb: effectiveMediaMaxMb,
709
+ canHostMedia,
710
+ webhookUrl: effectiveWebhookUrl,
711
+ webhookPath: path,
712
+ statusSink,
713
+ fetcher,
714
+ turnAdoptionLifecycle
715
+ });
716
+ }
717
+ });
718
+ ingress.start();
719
+ asyncStopHandlers.push(ingress.stop);
722
720
  const unregister = registerZaloWebhookTarget({
723
- token,
724
721
  account,
725
722
  config,
726
723
  runtime,
727
- core,
728
724
  path,
729
- webhookUrl: effectiveWebhookUrl,
730
- webhookPath: path,
731
725
  secret: webhookSecret,
732
- statusSink: (patch) => statusSink?.(patch),
733
- mediaMaxMb: effectiveMediaMaxMb,
734
- canHostMedia,
735
- fetcher
726
+ acceptWebhook: ingress.accept
736
727
  }, { route: {
737
728
  auth: "plugin",
738
729
  match: "exact",
@@ -749,6 +740,25 @@ async function monitorZaloProvider(options) {
749
740
  }
750
741
  } });
751
742
  stopHandlers.push(unregister);
743
+ await setWebhook(token, {
744
+ url: effectiveWebhookUrl,
745
+ secret_token: webhookSecret
746
+ }, fetcher);
747
+ let webhookCleanupPromise;
748
+ cleanupWebhook = async () => {
749
+ if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
750
+ runtime.log?.(`[${account.accountId}] Zalo stopping; deleting webhook`);
751
+ try {
752
+ await deleteWebhook(token, fetcher, WEBHOOK_CLEANUP_TIMEOUT_MS);
753
+ runtime.log?.(`[${account.accountId}] Zalo webhook deleted`);
754
+ } catch (err) {
755
+ const detail = err instanceof Error && err.name === "AbortError" ? `timed out after ${String(WEBHOOK_CLEANUP_TIMEOUT_MS)}ms` : formatZaloError(err);
756
+ runtime.error?.(`[${account.accountId}] Zalo webhook delete failed: ${detail}`);
757
+ }
758
+ })();
759
+ await webhookCleanupPromise;
760
+ };
761
+ runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
752
762
  await waitForAbortSignal(abortSignal);
753
763
  return;
754
764
  }
@@ -792,6 +802,7 @@ async function monitorZaloProvider(options) {
792
802
  abortSignal.removeEventListener("abort", stopOnAbort);
793
803
  await cleanupWebhook?.();
794
804
  stop();
805
+ for (const stopAsync of asyncStopHandlers) await stopAsync();
795
806
  runtime.log?.(`[${account.accountId}] Zalo provider stopped mode=${mode}`);
796
807
  }
797
808
  }
@@ -1,9 +1,8 @@
1
- import { B as registerWebhookTargetWithPluginRoute, I as readJsonWebhookBodyOrReject, K as resolveWebhookTargetWithAuthOrRejectSync, Q as withResolvedWebhookRequestPipeline, V as resolveClientIp, 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-CAA4ZtqF.js";
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-B53PGWBW.js";
2
+ import { ZaloWebhookPayloadError } from "./webhook-spool-Ozf0tcbs.js";
2
3
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
3
- import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
4
- import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
4
+ import { readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
5
5
  //#region extensions/zalo/src/monitor.webhook.ts
6
- const ZALO_WEBHOOK_REPLAY_WINDOW_MS = 5 * 6e4;
7
6
  const webhookTargets = /* @__PURE__ */ new Map();
8
7
  const webhookRateLimiter = createFixedWindowRateLimiter({
9
8
  windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
@@ -17,7 +16,6 @@ const webhookAnomalyTracker = createWebhookAnomalyTracker({
17
16
  });
18
17
  function clearZaloWebhookSecurityStateForTest() {
19
18
  webhookRateLimiter.clear();
20
- recentWebhookEvents.clearMemory();
21
19
  webhookAnomalyTracker.clear();
22
20
  }
23
21
  function getZaloWebhookRateLimitStateSizeForTest() {
@@ -26,46 +24,6 @@ function getZaloWebhookRateLimitStateSizeForTest() {
26
24
  function getZaloWebhookStatusCounterSizeForTest() {
27
25
  return webhookAnomalyTracker.size();
28
26
  }
29
- function buildReplayEventCacheKey(target, update) {
30
- const messageId = update.message?.message_id;
31
- if (!messageId) return null;
32
- const chatId = update.message?.chat?.id ?? "";
33
- const senderId = update.message?.from?.id ?? "";
34
- return JSON.stringify([
35
- target.path,
36
- target.account.accountId,
37
- update.event_name,
38
- chatId,
39
- senderId,
40
- messageId
41
- ]);
42
- }
43
- const recentWebhookEvents = createChannelReplayGuard({
44
- dedupe: {
45
- ttlMs: ZALO_WEBHOOK_REPLAY_WINDOW_MS,
46
- memoryMaxSize: 5e3
47
- },
48
- buildReplayKey: ({ target, update }) => buildReplayEventCacheKey(target, update)
49
- });
50
- var ZaloRetryableWebhookError = class extends Error {
51
- constructor(message, options) {
52
- super(message, options);
53
- this.name = "ZaloRetryableWebhookError";
54
- }
55
- };
56
- async function processZaloReplayGuardedUpdate(params) {
57
- const event = {
58
- target: params.target,
59
- update: params.update
60
- };
61
- return (await recentWebhookEvents.processGuarded(event, async () => {
62
- params.target.statusSink?.({ lastInboundAt: Date.now() });
63
- await params.processUpdate(event);
64
- }, {
65
- dedupe: { now: params.nowMs },
66
- onError: (error) => error instanceof ZaloRetryableWebhookError ? "release" : "commit"
67
- })).kind === "processed" ? "processed" : "duplicate";
68
- }
69
27
  function recordWebhookStatus(runtime, path, statusCode) {
70
28
  webhookAnomalyTracker.record({
71
29
  key: `${path}:${statusCode}`,
@@ -86,7 +44,7 @@ function registerZaloWebhookTarget(target, opts) {
86
44
  }).unregister;
87
45
  return registerWebhookTarget(webhookTargets, target, opts).unregister;
88
46
  }
89
- async function handleZaloWebhookRequest(req, res, processUpdate) {
47
+ async function handleZaloWebhookRequest(req, res) {
90
48
  return await withResolvedWebhookRequestPipeline({
91
49
  req,
92
50
  res,
@@ -102,13 +60,12 @@ async function handleZaloWebhookRequest(req, res, processUpdate) {
102
60
  trustedProxies,
103
61
  allowRealIpFallback
104
62
  }) ?? req.socket.remoteAddress ?? "unknown"}`;
105
- const nowMs = Date.now();
106
63
  if (!applyBasicWebhookRequestGuards({
107
64
  req,
108
65
  res,
109
66
  rateLimiter: webhookRateLimiter,
110
67
  rateLimitKey,
111
- nowMs
68
+ nowMs: Date.now()
112
69
  })) {
113
70
  recordWebhookStatus(targets[0]?.runtime, path, res.statusCode);
114
71
  return true;
@@ -131,40 +88,38 @@ async function handleZaloWebhookRequest(req, res, processUpdate) {
131
88
  recordWebhookStatus(target.runtime, path, res.statusCode);
132
89
  return true;
133
90
  }
134
- const body = await readJsonWebhookBodyOrReject({
91
+ const body = await readWebhookBodyOrReject({
135
92
  req,
136
93
  res,
137
94
  maxBytes: 1024 * 1024,
138
95
  timeoutMs: 3e4,
139
- emptyObjectOnEmpty: false,
140
- invalidJsonMessage: "Bad Request"
96
+ invalidBodyMessage: "Bad Request"
141
97
  });
142
98
  if (!body.ok) {
143
99
  recordWebhookStatus(target.runtime, path, res.statusCode);
144
100
  return true;
145
101
  }
146
- const raw = body.value;
147
- const record = raw && typeof raw === "object" ? raw : null;
148
- const update = record && record.ok === true && record.result ? record.result : record ?? void 0;
149
- if (!update?.event_name) {
150
- res.statusCode = 400;
151
- res.end("Bad Request");
102
+ try {
103
+ await target.acceptWebhook(body.value);
104
+ } catch (error) {
105
+ res.statusCode = error instanceof ZaloWebhookPayloadError ? 400 : 500;
106
+ res.end(res.statusCode === 400 ? "Bad Request" : "Internal Server Error");
152
107
  recordWebhookStatus(target.runtime, path, res.statusCode);
108
+ target.runtime.error?.(`[${target.account.accountId}] Zalo webhook admission failed: ${String(error)}`);
153
109
  return true;
154
110
  }
155
- runDetachedWebhookWork(() => processZaloReplayGuardedUpdate({
156
- target,
157
- update,
158
- processUpdate,
159
- nowMs
160
- })).catch((err) => {
161
- target.runtime.error?.(`[${target.account.accountId}] Zalo webhook failed: ${String(err)}`);
162
- });
163
111
  res.statusCode = 200;
164
112
  res.end("ok");
165
113
  return true;
166
114
  }
167
115
  });
168
116
  }
117
+ const zaloWebhookRuntime = {
118
+ clearZaloWebhookSecurityStateForTest,
119
+ getZaloWebhookRateLimitStateSizeForTest,
120
+ getZaloWebhookStatusCounterSizeForTest,
121
+ handleZaloWebhookRequest,
122
+ registerZaloWebhookTarget
123
+ };
169
124
  //#endregion
170
- export { ZaloRetryableWebhookError, clearZaloWebhookSecurityStateForTest, getZaloWebhookRateLimitStateSizeForTest, getZaloWebhookStatusCounterSizeForTest, handleZaloWebhookRequest, registerZaloWebhookTarget };
125
+ export { zaloWebhookRuntime };
@@ -10,7 +10,6 @@ import { addWildcardAllowFrom as addWildcardAllowFrom$1, applyAccountNameToChann
10
10
  import { resolveDefaultGroupPolicy as resolveDefaultGroupPolicy$1, resolveOpenProviderRuntimeGroupPolicy as resolveOpenProviderRuntimeGroupPolicy$1, warnMissingProviderGroupPolicyFallbackOnce as warnMissingProviderGroupPolicyFallbackOnce$1 } from "openclaw/plugin-sdk/runtime-group-policy";
11
11
  import { createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
12
12
  import { logTypingFailure as logTypingFailure$1 } from "openclaw/plugin-sdk/channel-feedback";
13
- import { resolveInboundRouteEnvelopeBuilderWithRuntime as resolveInboundRouteEnvelopeBuilderWithRuntime$1 } from "openclaw/plugin-sdk/inbound-envelope";
14
13
  import { waitForAbortSignal } from "openclaw/plugin-sdk/runtime";
15
14
  import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, readJsonWebhookBodyOrReject, registerPluginHttpRoute as registerPluginHttpRoute$1, registerWebhookTarget, registerWebhookTargetWithPluginRoute, resolveWebhookPath as resolveWebhookPath$1, resolveWebhookTargetWithAuthOrRejectSync, withResolvedWebhookRequestPipeline } from "openclaw/plugin-sdk/webhook-ingress";
16
15
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
@@ -20,4 +19,4 @@ const { setRuntime: setZaloRuntime, getRuntime: getZaloRuntime } = createPluginR
20
19
  errorMessage: "Zalo runtime not initialized"
21
20
  });
22
21
  //#endregion
23
- export { mergeAllowFromEntries$1 as A, registerWebhookTargetWithPluginRoute as B, formatAllowFromLowercase$1 as C, isNumericTargetId as D, isNormalizedSenderAllowed as E, promptSingleChannelSecretInput$1 as F, resolveWebhookPath$1 as G, resolveDefaultGroupPolicy$1 as H, readJsonWebhookBodyOrReject as I, sendPayloadWithChunkedTextAndMedia$1 as J, resolveWebhookTargetWithAuthOrRejectSync as K, readStringParam as L, normalizeAccountId$1 as M, normalizeResolvedSecretInputString as N, jsonResult as O, normalizeSecretInputString as P, withResolvedWebhookRequestPipeline as Q, registerPluginHttpRoute$1 as R, deliverTextOrMediaReply$1 as S, hasConfiguredSecretInput$1 as T, resolveInboundRouteEnvelopeBuilderWithRuntime$1 as U, resolveClientIp as V, resolveOpenProviderRuntimeGroupPolicy$1 as W, waitForAbortSignal as X, setTopLevelChannelDmPolicyWithAllowFrom as Y, warnMissingProviderGroupPolicyFallbackOnce$1 as Z, createChannelMessageReplyPipeline as _, WEBHOOK_ANOMALY_COUNTER_DEFAULTS as a, createFixedWindowRateLimiter as b, applyAccountNameToChannelSection as c, buildBaseAccountStatusSnapshot as d, buildChannelConfigSchema as f, chunkTextForOutbound$1 as g, buildTokenChannelStatusSummary$1 as h, PAIRING_APPROVED_MESSAGE as i, migrateBaseNameToDefaultAccount as j, logTypingFailure$1 as k, applyBasicWebhookRequestGuards as l, buildSingleChannelSecretPromptState$1 as m, setZaloRuntime as n, WEBHOOK_RATE_LIMIT_DEFAULTS as o, buildSecretInputSchema as p, runSingleChannelSecretStep$1 as q, DEFAULT_ACCOUNT_ID$1 as r, addWildcardAllowFrom$1 as s, getZaloRuntime as t, applySetupAccountConfigPatch as u, createChannelPairingController$1 as v, formatPairingApproveHint as w, createWebhookAnomalyTracker as x, createDedupeCache as y, registerWebhookTarget as z };
22
+ export { mergeAllowFromEntries$1 as A, registerWebhookTargetWithPluginRoute as B, formatAllowFromLowercase$1 as C, isNumericTargetId as D, isNormalizedSenderAllowed as E, promptSingleChannelSecretInput$1 as F, resolveWebhookTargetWithAuthOrRejectSync as G, resolveDefaultGroupPolicy$1 as H, readJsonWebhookBodyOrReject as I, setTopLevelChannelDmPolicyWithAllowFrom as J, runSingleChannelSecretStep$1 as K, readStringParam as L, normalizeAccountId$1 as M, normalizeResolvedSecretInputString as N, jsonResult as O, normalizeSecretInputString as P, registerPluginHttpRoute$1 as R, deliverTextOrMediaReply$1 as S, hasConfiguredSecretInput$1 as T, resolveOpenProviderRuntimeGroupPolicy$1 as U, resolveClientIp as V, resolveWebhookPath$1 as W, warnMissingProviderGroupPolicyFallbackOnce$1 as X, waitForAbortSignal as Y, withResolvedWebhookRequestPipeline as Z, createChannelMessageReplyPipeline as _, WEBHOOK_ANOMALY_COUNTER_DEFAULTS as a, createFixedWindowRateLimiter as b, applyAccountNameToChannelSection as c, buildBaseAccountStatusSnapshot as d, buildChannelConfigSchema as f, chunkTextForOutbound$1 as g, buildTokenChannelStatusSummary$1 as h, PAIRING_APPROVED_MESSAGE as i, migrateBaseNameToDefaultAccount as j, logTypingFailure$1 as k, applyBasicWebhookRequestGuards as l, buildSingleChannelSecretPromptState$1 as m, setZaloRuntime as n, WEBHOOK_RATE_LIMIT_DEFAULTS as o, buildSecretInputSchema as p, sendPayloadWithChunkedTextAndMedia$1 as q, DEFAULT_ACCOUNT_ID$1 as r, addWildcardAllowFrom$1 as s, getZaloRuntime as t, applySetupAccountConfigPatch as u, createChannelPairingController$1 as v, formatPairingApproveHint as w, createWebhookAnomalyTracker as x, createDedupeCache as y, registerWebhookTarget as z };
@@ -1,2 +1,2 @@
1
- import { A as mergeAllowFromEntries, B as registerWebhookTargetWithPluginRoute, C as formatAllowFromLowercase, D as isNumericTargetId, E as isNormalizedSenderAllowed, F as promptSingleChannelSecretInput, G as resolveWebhookPath, H as resolveDefaultGroupPolicy, I as readJsonWebhookBodyOrReject, J as sendPayloadWithChunkedTextAndMedia, K as resolveWebhookTargetWithAuthOrRejectSync, L as readStringParam, M as normalizeAccountId, N as normalizeResolvedSecretInputString, O as jsonResult, P as normalizeSecretInputString, Q as withResolvedWebhookRequestPipeline, R as registerPluginHttpRoute, S as deliverTextOrMediaReply, T as hasConfiguredSecretInput, U as resolveInboundRouteEnvelopeBuilderWithRuntime, V as resolveClientIp, W as resolveOpenProviderRuntimeGroupPolicy, X as waitForAbortSignal, Y as setTopLevelChannelDmPolicyWithAllowFrom, Z as warnMissingProviderGroupPolicyFallbackOnce, _ as createChannelMessageReplyPipeline, a as WEBHOOK_ANOMALY_COUNTER_DEFAULTS, b as createFixedWindowRateLimiter, c as applyAccountNameToChannelSection, d as buildBaseAccountStatusSnapshot, f as buildChannelConfigSchema, g as chunkTextForOutbound, h as buildTokenChannelStatusSummary, i as PAIRING_APPROVED_MESSAGE, j as migrateBaseNameToDefaultAccount, k as logTypingFailure, l as applyBasicWebhookRequestGuards, m as buildSingleChannelSecretPromptState, n as setZaloRuntime, o as WEBHOOK_RATE_LIMIT_DEFAULTS, p as buildSecretInputSchema, q as runSingleChannelSecretStep, r as DEFAULT_ACCOUNT_ID, s as addWildcardAllowFrom, u as applySetupAccountConfigPatch, v as createChannelPairingController, w as formatPairingApproveHint, x as createWebhookAnomalyTracker, y as createDedupeCache, z as registerWebhookTarget } from "./runtime-api-CAA4ZtqF.js";
2
- export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, addWildcardAllowFrom, applyAccountNameToChannelSection, applyBasicWebhookRequestGuards, applySetupAccountConfigPatch, buildBaseAccountStatusSnapshot, buildChannelConfigSchema, buildSecretInputSchema, buildSingleChannelSecretPromptState, buildTokenChannelStatusSummary, chunkTextForOutbound, createChannelMessageReplyPipeline, createChannelPairingController, createDedupeCache, createFixedWindowRateLimiter, createWebhookAnomalyTracker, deliverTextOrMediaReply, formatAllowFromLowercase, formatPairingApproveHint, hasConfiguredSecretInput, isNormalizedSenderAllowed, isNumericTargetId, jsonResult, logTypingFailure, mergeAllowFromEntries, migrateBaseNameToDefaultAccount, normalizeAccountId, normalizeResolvedSecretInputString, normalizeSecretInputString, promptSingleChannelSecretInput, readJsonWebhookBodyOrReject, readStringParam, registerPluginHttpRoute, registerWebhookTarget, registerWebhookTargetWithPluginRoute, resolveClientIp, resolveDefaultGroupPolicy, resolveInboundRouteEnvelopeBuilderWithRuntime, resolveOpenProviderRuntimeGroupPolicy, resolveWebhookPath, resolveWebhookTargetWithAuthOrRejectSync, runSingleChannelSecretStep, sendPayloadWithChunkedTextAndMedia, setTopLevelChannelDmPolicyWithAllowFrom, setZaloRuntime, waitForAbortSignal, warnMissingProviderGroupPolicyFallbackOnce, withResolvedWebhookRequestPipeline };
1
+ import { A as mergeAllowFromEntries, B as registerWebhookTargetWithPluginRoute, C as formatAllowFromLowercase, D as isNumericTargetId, E as isNormalizedSenderAllowed, F as promptSingleChannelSecretInput, G as resolveWebhookTargetWithAuthOrRejectSync, H as resolveDefaultGroupPolicy, I as readJsonWebhookBodyOrReject, J as setTopLevelChannelDmPolicyWithAllowFrom, K as runSingleChannelSecretStep, L as readStringParam, M as normalizeAccountId, N as normalizeResolvedSecretInputString, O as jsonResult, P as normalizeSecretInputString, R as registerPluginHttpRoute, S as deliverTextOrMediaReply, T as hasConfiguredSecretInput, U as resolveOpenProviderRuntimeGroupPolicy, V as resolveClientIp, W as resolveWebhookPath, X as warnMissingProviderGroupPolicyFallbackOnce, Y as waitForAbortSignal, Z as withResolvedWebhookRequestPipeline, _ as createChannelMessageReplyPipeline, a as WEBHOOK_ANOMALY_COUNTER_DEFAULTS, b as createFixedWindowRateLimiter, c as applyAccountNameToChannelSection, d as buildBaseAccountStatusSnapshot, f as buildChannelConfigSchema, g as chunkTextForOutbound, h as buildTokenChannelStatusSummary, i as PAIRING_APPROVED_MESSAGE, j as migrateBaseNameToDefaultAccount, k as logTypingFailure, l as applyBasicWebhookRequestGuards, m as buildSingleChannelSecretPromptState, n as setZaloRuntime, o as WEBHOOK_RATE_LIMIT_DEFAULTS, p as buildSecretInputSchema, q as sendPayloadWithChunkedTextAndMedia, r as DEFAULT_ACCOUNT_ID, s as addWildcardAllowFrom, u as applySetupAccountConfigPatch, v as createChannelPairingController, w as formatPairingApproveHint, x as createWebhookAnomalyTracker, y as createDedupeCache, z as registerWebhookTarget } from "./runtime-api-B53PGWBW.js";
2
+ export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, addWildcardAllowFrom, applyAccountNameToChannelSection, applyBasicWebhookRequestGuards, applySetupAccountConfigPatch, buildBaseAccountStatusSnapshot, buildChannelConfigSchema, buildSecretInputSchema, buildSingleChannelSecretPromptState, buildTokenChannelStatusSummary, chunkTextForOutbound, createChannelMessageReplyPipeline, createChannelPairingController, createDedupeCache, createFixedWindowRateLimiter, createWebhookAnomalyTracker, deliverTextOrMediaReply, formatAllowFromLowercase, formatPairingApproveHint, hasConfiguredSecretInput, isNormalizedSenderAllowed, isNumericTargetId, jsonResult, logTypingFailure, mergeAllowFromEntries, migrateBaseNameToDefaultAccount, normalizeAccountId, normalizeResolvedSecretInputString, normalizeSecretInputString, promptSingleChannelSecretInput, readJsonWebhookBodyOrReject, readStringParam, registerPluginHttpRoute, registerWebhookTarget, registerWebhookTargetWithPluginRoute, resolveClientIp, resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, resolveWebhookPath, resolveWebhookTargetWithAuthOrRejectSync, runSingleChannelSecretStep, sendPayloadWithChunkedTextAndMedia, setTopLevelChannelDmPolicyWithAllowFrom, setZaloRuntime, waitForAbortSignal, warnMissingProviderGroupPolicyFallbackOnce, withResolvedWebhookRequestPipeline };
@@ -1,4 +1,4 @@
1
- import { d as resolveZaloToken, u as resolveZaloAccount } from "./setup-core-fwCh0QUi.js";
1
+ import { d as resolveZaloAccount, f as resolveZaloToken } from "./setup-core-BBA-V-j0.js";
2
2
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -6,6 +6,11 @@ import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
6
6
  import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
7
7
  import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
8
8
  import { makeProxyFetch } from "openclaw/plugin-sdk/fetch-runtime";
9
+ //#region extensions/zalo/src/timeouts.ts
10
+ const ZALO_DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
11
+ const ZALO_OUTBOUND_MEDIA_TTL_MS = 2 * 6e4;
12
+ const ZALO_SEND_PHOTO_REQUEST_TIMEOUT_MS = 15e4;
13
+ //#endregion
9
14
  //#region extensions/zalo/src/api.ts
10
15
  /**
11
16
  * Zalo Bot API client
@@ -45,8 +50,8 @@ function resolveZaloApiUrl(apiUrl) {
45
50
  async function callZaloApi(method, token, body, options) {
46
51
  const url = `${resolveZaloApiUrl(options?.apiUrl)}/bot${token}/${method}`;
47
52
  const controller = new AbortController();
48
- const requestTimeoutMs = options?.timeoutMs === void 0 ? void 0 : resolveTimerTimeoutMs(options.timeoutMs, 1);
49
- const timeoutId = requestTimeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(), requestTimeoutMs);
53
+ const requestTimeoutMs = resolveTimerTimeoutMs(options?.timeoutMs, ZALO_DEFAULT_REQUEST_TIMEOUT_MS);
54
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
50
55
  const fetcher = options?.fetch ?? fetch;
51
56
  try {
52
57
  const data = await readProviderJsonResponse(await fetcher(url, {
@@ -58,7 +63,7 @@ async function callZaloApi(method, token, body, options) {
58
63
  if (!data.ok) throw new ZaloApiError(data.description ?? `Zalo API error: ${method}`, data.error_code, data.description);
59
64
  return data;
60
65
  } finally {
61
- if (timeoutId) clearTimeout(timeoutId);
66
+ clearTimeout(timeoutId);
62
67
  }
63
68
  }
64
69
  /**
@@ -92,7 +97,10 @@ async function sendPhoto(token, params, fetcher) {
92
97
  return callZaloApi("sendPhoto", token, {
93
98
  ...params,
94
99
  photo: parsedPhotoUrl.href
95
- }, { fetch: fetcher });
100
+ }, {
101
+ timeoutMs: ZALO_SEND_PHOTO_REQUEST_TIMEOUT_MS,
102
+ fetch: fetcher
103
+ });
96
104
  }
97
105
  /**
98
106
  * Send a temporary chat action such as typing.
@@ -285,4 +293,4 @@ async function sendPhotoZalo(chatId, photoUrl, options = {}) {
285
293
  }, context.fetcher))());
286
294
  }
287
295
  //#endregion
288
- export { getMe as a, sendChatAction as c, setWebhook as d, deleteWebhook as i, sendMessage as l, resolveZaloProxyFetch as n, getUpdates as o, ZaloApiError as r, getWebhookInfo as s, sendMessageZalo as t, sendPhoto as u };
296
+ export { getMe as a, sendChatAction as c, setWebhook as d, ZALO_OUTBOUND_MEDIA_TTL_MS as f, deleteWebhook as i, sendMessage as l, resolveZaloProxyFetch as n, getUpdates as o, ZaloApiError as r, getWebhookInfo as s, sendMessageZalo as t, sendPhoto as u };
package/dist/setup-api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-fwCh0QUi.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-BBA-V-j0.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
@@ -4,6 +4,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runti
4
4
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
5
5
  import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
6
6
  import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
7
+ import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
7
8
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, addWildcardAllowFrom, createDelegatedSetupWizardProxy, createPatchedAccountSetupAdapter, createSetupInputPresenceValidator, createSetupTranslator, formatDocsLink, mergeAllowFromEntries, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/setup";
8
9
  //#region \0rolldown/runtime.js
9
10
  var __defProp = Object.defineProperty;
@@ -184,16 +185,47 @@ async function promptZaloAllowFrom(params) {
184
185
  //#region extensions/zalo/src/setup-core.ts
185
186
  const t = createSetupTranslator();
186
187
  const channel = "zalo";
187
- const zaloSetupAdapter = createPatchedAccountSetupAdapter({
188
- channelKey: channel,
189
- validateInput: createSetupInputPresenceValidator({
190
- defaultAccountOnlyEnvError: "ZALO_BOT_TOKEN can only be used for the default account.",
191
- whenNotUseEnv: [{
192
- someOf: ["token", "tokenFile"],
193
- message: "Zalo requires token or --token-file (or --use-env)."
194
- }]
188
+ const zaloSetupAdapter = {
189
+ ...createPatchedAccountSetupAdapter({
190
+ channelKey: channel,
191
+ validateInput: createSetupInputPresenceValidator({
192
+ defaultAccountOnlyEnvError: "ZALO_BOT_TOKEN can only be used for the default account.",
193
+ whenNotUseEnv: [{
194
+ someOf: ["token", "tokenFile"],
195
+ message: "Zalo requires token or --token-file (or --use-env)."
196
+ }]
197
+ }),
198
+ buildPatch: (input) => input.useEnv ? {} : input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {}
195
199
  }),
196
- buildPatch: (input) => input.useEnv ? {} : input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {}
200
+ singleAccountKeysToMove: ["webhookSecret", "tokenFile"]
201
+ };
202
+ const zaloSetupContract = defineChannelSetupContract({
203
+ fields: {
204
+ token: {
205
+ kind: "string",
206
+ sensitive: true,
207
+ cli: {
208
+ flags: "--token <token>",
209
+ description: "Zalo bot token"
210
+ }
211
+ },
212
+ tokenFile: {
213
+ kind: "string",
214
+ sensitive: true,
215
+ cli: {
216
+ flags: "--token-file <path>",
217
+ description: "Zalo bot token file"
218
+ }
219
+ },
220
+ useEnv: {
221
+ kind: "boolean",
222
+ cli: {
223
+ flags: "--use-env",
224
+ description: "Use ZALO_BOT_TOKEN"
225
+ }
226
+ }
227
+ },
228
+ legacyAdapter: zaloSetupAdapter
197
229
  });
198
230
  const zaloDmPolicy = {
199
231
  label: "Zalo",
@@ -284,4 +316,4 @@ function createZaloSetupWizardProxy(loadWizard) {
284
316
  });
285
317
  }
286
318
  //#endregion
287
- export { promptZaloAllowFrom as a, listZaloAccountIds as c, resolveZaloToken as d, buildSecretInputSchema as f, noteZaloTokenHelp as i, resolveDefaultZaloAccountId as l, zaloDmPolicy as n, accounts_exports as o, normalizeSecretInputString as p, zaloSetupAdapter as r, listEnabledZaloAccounts as s, createZaloSetupWizardProxy as t, resolveZaloAccount as u };
319
+ 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 };
@@ -1,4 +1,4 @@
1
- import { a as promptZaloAllowFrom, i as noteZaloTokenHelp, n as zaloDmPolicy, u as resolveZaloAccount } from "./setup-core-fwCh0QUi.js";
1
+ import { a as noteZaloTokenHelp, d as resolveZaloAccount, n as zaloDmPolicy, o as promptZaloAllowFrom } from "./setup-core-BBA-V-j0.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
4
  const t = createSetupTranslator();
@@ -0,0 +1,174 @@
1
+ import { t as getZaloRuntime } from "./runtime-api-B53PGWBW.js";
2
+ import { r as ZaloApiError } from "./send-WPSy5EgF.js";
3
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
4
+ import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
5
+ //#region extensions/zalo/src/webhook-spool.ts
6
+ const ZALO_WEBHOOK_SPOOL_VERSION = 1;
7
+ const ZALO_WEBHOOK_DRAIN_INTERVAL_MS = 500;
8
+ 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
+ }
26
+ function parseRawRecord(rawEvent) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(rawEvent);
30
+ } catch (error) {
31
+ throw new ZaloWebhookPayloadError("Zalo webhook body contains invalid JSON.", { cause: error });
32
+ }
33
+ if (!isRecord(parsed)) throw new ZaloWebhookPayloadError("Zalo webhook body must be a JSON object.");
34
+ return parsed;
35
+ }
36
+ function resolveUpdateRecord(envelope) {
37
+ if (envelope.ok === true && isRecord(envelope.result)) return envelope.result;
38
+ return envelope;
39
+ }
40
+ function inspectZaloWebhookEvent(rawEvent) {
41
+ const update = resolveUpdateRecord(parseRawRecord(rawEvent));
42
+ const message = isRecord(update.message) ? update.message : null;
43
+ const eventId = nonEmptyString(message?.message_id);
44
+ if (!eventId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.message_id.");
45
+ const chatId = nonEmptyString((isRecord(message?.chat) ? message.chat : null)?.id);
46
+ if (!chatId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.chat.id.");
47
+ return {
48
+ eventId,
49
+ laneKey: `chat:${chatId}`,
50
+ update
51
+ };
52
+ }
53
+ function parseClaimedUpdate(payload, claimedId) {
54
+ if (payload.version !== ZALO_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string") throw new ZaloWebhookPayloadError("Zalo webhook spool payload is invalid.");
55
+ const facts = inspectZaloWebhookEvent(payload.rawEvent);
56
+ if (facts.eventId !== claimedId) throw new ZaloWebhookPayloadError("Zalo webhook message id changed after durable admission.");
57
+ const eventName = nonEmptyString(facts.update.event_name);
58
+ 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
+ const message = facts.update.message;
60
+ const from = isRecord(message.from) ? message.from : null;
61
+ const chat = isRecord(message.chat) ? message.chat : null;
62
+ if (!nonEmptyString(from?.id)) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.from.id.");
63
+ if (chat?.chat_type !== "PRIVATE" && chat?.chat_type !== "GROUP") throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid chat type.");
64
+ if (typeof message.date !== "number" || !Number.isFinite(message.date)) throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid date.");
65
+ if (eventName === "message.text.received" && typeof message.text !== "string") throw new ZaloWebhookPayloadError("Zalo text event is missing message.text.");
66
+ return facts.update;
67
+ }
68
+ function errorText(error) {
69
+ return error instanceof Error ? error.message : String(error);
70
+ }
71
+ function isZaloAuthenticationFailure(error) {
72
+ let current = error;
73
+ const seen = /* @__PURE__ */ new Set();
74
+ while (current && typeof current === "object" && !seen.has(current)) {
75
+ seen.add(current);
76
+ const candidate = current;
77
+ if (current instanceof ZaloApiError && (current.errorCode === 401 || current.errorCode === 403) || candidate.status === 401 || candidate.status === 403 || candidate.statusCode === 401 || candidate.statusCode === 403) return true;
78
+ current = candidate.cause;
79
+ }
80
+ return false;
81
+ }
82
+ function createZaloWebhookIngress(options) {
83
+ const queue = options.queue ?? getZaloRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
84
+ const deferredClaims = /* @__PURE__ */ new Map();
85
+ const monitor = createChannelIngressMonitor({
86
+ queue,
87
+ inspect: (rawEvent) => inspectZaloWebhookEvent(rawEvent),
88
+ payload: {
89
+ storage: "raw-event",
90
+ version: ZALO_WEBHOOK_SPOOL_VERSION,
91
+ serialize: (rawEvent) => rawEvent,
92
+ deserialize: (rawEvent) => rawEvent,
93
+ createClaimError: (kind) => new ZaloWebhookPayloadError(kind === "invalid-version" ? "Zalo webhook spool payload is invalid." : "Zalo webhook identity changed after durable admission.")
94
+ },
95
+ deliver: async (_rawEvent, lifecycle, claim) => {
96
+ 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" };
127
+ },
128
+ 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
+ },
136
+ waitForDeliveryIdleBeforeRepump: false,
137
+ runPumpTask: runDetachedWebhookWork,
138
+ drain: {
139
+ adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
140
+ startLimit: ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
141
+ retryPolicy: {
142
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
143
+ deadLetterMinAgeMs: 0
144
+ },
145
+ resolveNonRetryableFailure: (error) => {
146
+ if (error instanceof ZaloWebhookPayloadError) return {
147
+ reason: "invalid-event",
148
+ message: error.message
149
+ };
150
+ if (isZaloAuthenticationFailure(error)) return {
151
+ reason: "authentication-failed",
152
+ message: errorText(error)
153
+ };
154
+ return null;
155
+ },
156
+ onLog: (message) => options.runtime.error?.(`zalo ingress: ${message}`)
157
+ },
158
+ createStoppedError: () => /* @__PURE__ */ new Error("Zalo ingress stopped."),
159
+ onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${errorText(error)}`)
160
+ });
161
+ return {
162
+ accept: async (rawEvent) => {
163
+ await monitor.admit(rawEvent);
164
+ },
165
+ start: monitor.start,
166
+ stop: async () => {
167
+ await monitor.stop();
168
+ await Promise.allSettled(deferredClaims.values());
169
+ }
170
+ };
171
+ }
172
+ const zaloWebhookIngressRuntime = { createZaloWebhookIngress };
173
+ //#endregion
174
+ export { ZaloWebhookPayloadError, zaloWebhookIngressRuntime };
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@openclaw/zalo",
3
- "version": "2026.7.2-beta.2",
3
+ "version": "2026.7.2-beta.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/zalo",
9
- "version": "2026.7.2-beta.2",
9
+ "version": "2026.7.2-beta.4",
10
10
  "dependencies": {
11
11
  "zod": "4.4.3"
12
12
  },
13
13
  "peerDependencies": {
14
- "openclaw": ">=2026.7.2-beta.2"
14
+ "openclaw": ">=2026.7.2-beta.4"
15
15
  },
16
16
  "peerDependenciesMeta": {
17
17
  "openclaw": {
@@ -9,12 +9,6 @@
9
9
  "channels": [
10
10
  "zalo"
11
11
  ],
12
- "channelEnvVars": {
13
- "zalo": [
14
- "ZALO_BOT_TOKEN",
15
- "ZALO_WEBHOOK_SECRET"
16
- ]
17
- },
18
12
  "configSchema": {
19
13
  "type": "object",
20
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/zalo",
3
- "version": "2026.7.2-beta.2",
3
+ "version": "2026.7.2-beta.4",
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.2"
11
+ "openclaw": ">=2026.7.2-beta.4"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -20,8 +20,19 @@
20
20
  "./index.ts"
21
21
  ],
22
22
  "setupEntry": "./dist/setup-entry.js",
23
+ "setupFeatures": {
24
+ "configPromotion": true
25
+ },
23
26
  "channel": {
24
27
  "id": "zalo",
28
+ "configuredState": {
29
+ "env": {
30
+ "anyOf": [
31
+ "ZALO_BOT_TOKEN",
32
+ "ZALO_WEBHOOK_SECRET"
33
+ ]
34
+ }
35
+ },
25
36
  "label": "Zalo",
26
37
  "selectionLabel": "Zalo (Bot API)",
27
38
  "docsPath": "/channels/zalo",
@@ -31,7 +42,37 @@
31
42
  "zl"
32
43
  ],
33
44
  "order": 80,
34
- "quickstartAllowFrom": true
45
+ "quickstartAllowFrom": true,
46
+ "setup": {
47
+ "fields": [
48
+ {
49
+ "key": "token",
50
+ "kind": "string",
51
+ "sensitive": true,
52
+ "cli": {
53
+ "flags": "--token <token>",
54
+ "description": "Zalo bot token"
55
+ }
56
+ },
57
+ {
58
+ "key": "tokenFile",
59
+ "kind": "string",
60
+ "sensitive": true,
61
+ "cli": {
62
+ "flags": "--token-file <path>",
63
+ "description": "Zalo bot token file"
64
+ }
65
+ },
66
+ {
67
+ "key": "useEnv",
68
+ "kind": "boolean",
69
+ "cli": {
70
+ "flags": "--use-env",
71
+ "description": "Use ZALO_BOT_TOKEN"
72
+ }
73
+ }
74
+ ]
75
+ }
35
76
  },
36
77
  "install": {
37
78
  "npmSpec": "@openclaw/zalo",
@@ -39,10 +80,10 @@
39
80
  "minHostVersion": ">=2026.4.10"
40
81
  },
41
82
  "compat": {
42
- "pluginApi": ">=2026.7.2-beta.2"
83
+ "pluginApi": ">=2026.7.2-beta.4"
43
84
  },
44
85
  "build": {
45
- "openclawVersion": "2026.7.2-beta.2"
86
+ "openclawVersion": "2026.7.2-beta.4"
46
87
  },
47
88
  "release": {
48
89
  "publishToClawHub": true,