@openclaw/zalo 2026.7.2-beta.7 → 2026.8.1-beta.3

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-BUvbbedq.js";
1
+ import { t as sendMessageZalo } from "./send-DVMIplpy.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-LzhuTlsD.js";
2
- import { t as zaloPlugin } from "./channel-idqJI6YK.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-CcCJ1E26.js";
2
+ import { t as zaloPlugin } from "./channel-Vdxu1Y3k.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,12 +1,12 @@
1
- import { c as inspectZaloAccount, d as resolveZaloAccount, i as zaloSetupContract, l as listZaloAccountIds, p as buildSecretInputSchema, r as zaloSetupAdapter, t as createZaloSetupWizardProxy, u as resolveDefaultZaloAccountId } from "./setup-core-LzhuTlsD.js";
2
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-Bxvjh0Hq.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-CcCJ1E26.js";
2
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-Cpsqe5Cv.js";
3
3
  import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
4
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
5
5
  import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
6
6
  import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver, mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers";
7
7
  import { buildChannelConfigSchema, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
8
8
  import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
9
- import { buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyWarning, createOpenProviderGroupPolicyWarningCollector } from "openclaw/plugin-sdk/channel-policy";
9
+ import { buildOpenGroupPolicyRestrictSendersWarning, buildOpenGroupPolicyWarning, createConditionalWarningCollector, createOpenProviderGroupPolicyWarningCollector } from "openclaw/plugin-sdk/channel-policy";
10
10
  import { createAttachedChannelResultAdapter, createEmptyChannelResult } from "openclaw/plugin-sdk/channel-send-result";
11
11
  import { buildTokenChannelStatusSummary } from "openclaw/plugin-sdk/channel-status";
12
12
  import { createStaticReplyToModeResolver } from "openclaw/plugin-sdk/conversation-runtime";
@@ -16,15 +16,15 @@ import { sendPayloadWithChunkedTextAndMedia } from "openclaw/plugin-sdk/reply-pa
16
16
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, standardDmPolicyOpenIssue } from "openclaw/plugin-sdk/status-helpers";
17
17
  import { chunkTextForOutbound, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
18
18
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
19
- import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix, stripTargetKindPrefix } from "openclaw/plugin-sdk/core";
20
19
  import { jsonResult, readStringParam } from "openclaw/plugin-sdk/channel-actions";
21
20
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
22
21
  import { createChannelApprovalAuth } from "openclaw/plugin-sdk/approval-auth-runtime";
23
22
  import { AllowFromListSchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, buildMultiAccountChannelSchema } from "openclaw/plugin-sdk/channel-config-schema";
24
23
  import { z } from "zod";
24
+ import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix, stripTargetKindPrefix } from "openclaw/plugin-sdk/core";
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-BYBjeo4Q.js"), "zaloActionsRuntime");
27
+ const loadZaloActionsRuntime = createLazyRuntimeNamedExport(() => import("./actions.runtime-DUY9_6Ss.js"), "zaloActionsRuntime");
28
28
  const providerId = "zalo";
29
29
  const ZALO_ACTIONS = /* @__PURE__ */ new Set(["send"]);
30
30
  function listEnabledAccounts(cfg, accountId) {
@@ -92,6 +92,7 @@ const zaloApprovalAuth = createChannelApprovalAuth({
92
92
  const ZaloConfigSchema = buildMultiAccountChannelSchema(z.object({
93
93
  name: z.string().optional(),
94
94
  enabled: z.boolean().optional(),
95
+ configWrites: z.boolean().optional(),
95
96
  markdown: MarkdownConfigSchema,
96
97
  botToken: buildSecretInputSchema().optional(),
97
98
  tokenFile: z.string().optional(),
@@ -179,8 +180,8 @@ function looksLikeZaloChatId(raw, normalized) {
179
180
  const target = normalizeZaloMessagingTarget(normalized ?? raw);
180
181
  return Boolean(target);
181
182
  }
182
- const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-DsaLk085.js"));
183
- const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-CQMbpZye.js")).zaloSetupWizard);
183
+ const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-UD-M7eDb.js"));
184
+ const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-CIpp_MDf.js").then((n) => n.t)).zaloSetupWizard);
184
185
  const zaloTextChunkLimit = 2e3;
185
186
  async function sendZaloDelivery(ctx) {
186
187
  const result = await (await loadZaloChannelRuntime()).sendZaloText({
@@ -259,11 +260,16 @@ const collectZaloSecurityWarnings = createOpenProviderGroupPolicyWarningCollecto
259
260
  })];
260
261
  }
261
262
  });
263
+ const collectZaloOpenGroupFindings = createConditionalWarningCollector.findings({
264
+ collectWarnings: collectZaloSecurityWarnings,
265
+ checkId: "channels.zalo.groups.open",
266
+ severity: "critical",
267
+ title: "Zalo security warning"
268
+ });
262
269
  const zaloPlugin = createChatChannelPlugin({
263
270
  base: {
264
271
  id: "zalo",
265
272
  meta,
266
- setup: zaloSetupAdapter,
267
273
  setupContract: zaloSetupContract,
268
274
  setupWizard: zaloSetupWizard,
269
275
  capabilities: {
@@ -301,6 +307,10 @@ const zaloPlugin = createChatChannelPlugin({
301
307
  messaging: {
302
308
  targetPrefixes: ["zalo", "zl"],
303
309
  normalizeTarget: normalizeZaloMessagingTarget,
310
+ inferTargetChatType: ({ to }) => {
311
+ const target = normalizeZaloMessagingTarget(to);
312
+ return target ? /^group:/i.test(target) ? "group" : "direct" : void 0;
313
+ },
304
314
  resolveOutboundSessionRoute: (params) => resolveZaloOutboundSessionRoute(params),
305
315
  targetResolver: {
306
316
  looksLikeId: looksLikeZaloChatId,
@@ -345,7 +355,7 @@ const zaloPlugin = createChatChannelPlugin({
345
355
  },
346
356
  security: {
347
357
  resolveDmPolicy: resolveZaloDmPolicy,
348
- collectWarnings: collectZaloSecurityWarnings
358
+ collectWarnings: collectZaloOpenGroupFindings
349
359
  },
350
360
  pairing: { text: {
351
361
  idLabel: "zaloUserId",
@@ -1,2 +1,2 @@
1
- import { t as zaloPlugin } from "./channel-idqJI6YK.js";
1
+ import { t as zaloPlugin } from "./channel-Vdxu1Y3k.js";
2
2
  export { zaloPlugin };
@@ -1,58 +1,41 @@
1
- import { m as normalizeSecretInputString } from "./setup-core-LzhuTlsD.js";
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-BUvbbedq.js";
1
+ import { m as normalizeSecretInputString } from "./setup-core-CcCJ1E26.js";
2
+ import { i as PAIRING_APPROVED_MESSAGE } from "./runtime-api-BNim5FCa.js";
3
+ import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-DVMIplpy.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-LzhuTlsD.js").then((n) => n.s);
38
+ const { resolveZaloAccount } = await import("./setup-core-CcCJ1E26.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-C5cximHM.js");
72
+ const { monitorZaloProvider } = await import("./monitor-JdVVhnCm.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
- 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-BUvbbedq.js";
2
+ import { t as getZaloRuntime } from "./runtime-api-BNim5FCa.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-DVMIplpy.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-D56OCa3C.js"), "zaloWebhookRuntime");
167
- const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-DzCPbb3O.js"), "zaloWebhookIngressRuntime");
177
+ const loadZaloWebhookRuntime = createLazyRuntimeNamedExport(() => import("./monitor.webhook-cl1sV6j0.js"), "zaloWebhookRuntime");
178
+ const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-Bdnu4GjY.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);
@@ -684,7 +730,6 @@ async function monitorZaloProvider(options) {
684
730
  if (hostedMediaRoutePath) {
685
731
  const unregisterHostedMediaRoute = registerSharedHostedMediaRoute({
686
732
  path: hostedMediaRoutePath,
687
- accountId: account.accountId,
688
733
  log: runtime.log
689
734
  });
690
735
  stopHandlers.push(unregisterHostedMediaRoute);
@@ -735,6 +780,7 @@ async function monitorZaloProvider(options) {
735
780
  source: "zalo-webhook",
736
781
  accountId: account.accountId,
737
782
  log: runtime.log,
783
+ throwOnFailure: true,
738
784
  handler: async (req, res) => {
739
785
  if (!await handleZaloWebhookRequest(req, res) && !res.headersSent) {
740
786
  res.statusCode = 404;
@@ -748,6 +794,7 @@ async function monitorZaloProvider(options) {
748
794
  url: effectiveWebhookUrl,
749
795
  secret_token: webhookSecret
750
796
  }, fetcher);
797
+ statusSink?.(channelReadyPatch());
751
798
  let webhookCleanupPromise;
752
799
  cleanupWebhook = async () => {
753
800
  if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
@@ -800,7 +847,13 @@ async function monitorZaloProvider(options) {
800
847
  });
801
848
  await waitForAbortSignal(abortSignal);
802
849
  } catch (err) {
803
- 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
+ });
804
857
  throw err;
805
858
  } finally {
806
859
  abortSignal.removeEventListener("abort", stopOnAbort);
@@ -1,5 +1,5 @@
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-DzCPbb3O.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-BNim5FCa.js";
2
+ import { ZaloWebhookPayloadError } from "./webhook-spool-Bdnu4GjY.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
@@ -20,9 +20,6 @@ function clearZaloWebhookSecurityStateForTest() {
20
20
  webhookRateLimiter.clear();
21
21
  webhookAnomalyTracker.clear();
22
22
  }
23
- function getZaloWebhookRateLimitStateSizeForTest() {
24
- return webhookRateLimiter.size();
25
- }
26
23
  function getZaloWebhookStatusCounterSizeForTest() {
27
24
  return webhookAnomalyTracker.size();
28
25
  }
@@ -119,7 +116,6 @@ async function handleZaloWebhookRequest(req, res) {
119
116
  }
120
117
  const zaloWebhookRuntime = {
121
118
  clearZaloWebhookSecurityStateForTest,
122
- getZaloWebhookRateLimitStateSizeForTest,
123
119
  getZaloWebhookStatusCounterSizeForTest,
124
120
  handleZaloWebhookRequest,
125
121
  registerZaloWebhookTarget
@@ -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 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-qxdOyk3M.js";
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-BNim5FCa.js";
2
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 { collectConditionalChannelFieldAssignments, createChannelSecretTargetRegistryEntries, getChannelSurface, hasOwnProperty } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
1
+ import { collectConditionalChannelFieldAssignments, createChannelSecretTargetRegistryEntries, getChannelSurface, hasOwnProperty, normalizeSecretStringValue } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
2
2
  //#region extensions/zalo/src/secret-contract.ts
3
3
  const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
4
4
  channelKey: "zalo",
@@ -17,7 +17,7 @@ function collectRuntimeConfigAssignments(params) {
17
17
  defaults: params.defaults,
18
18
  context: params.context,
19
19
  topLevelActiveWithoutAccounts: true,
20
- topLevelInheritedAccountActive: ({ account, enabled }) => enabled && !hasOwnProperty(account, "botToken"),
20
+ topLevelInheritedAccountActive: ({ account, enabled }) => enabled && !hasOwnProperty(account, "botToken") && !normalizeSecretStringValue(account.tokenFile),
21
21
  accountActive: ({ enabled }) => enabled,
22
22
  topInactiveReason: "no enabled Zalo surface inherits this top-level botToken.",
23
23
  accountInactiveReason: "Zalo account is disabled."
@@ -1,2 +1,2 @@
1
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-Bxvjh0Hq.js";
1
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-Cpsqe5Cv.js";
2
2
  export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };