@openclaw/zalo 2026.7.2-beta.7 → 2026.7.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actions.runtime-BYBjeo4Q.js → .setup/actions.runtime-C0KWbUJu.mjs} +1 -1
- package/dist/.setup/api-D2kRAHnC.mjs +6 -0
- package/dist/{channel-idqJI6YK.js → .setup/channel-ChIucoCG.mjs} +68 -66
- package/dist/{channel.runtime-DsaLk085.js → .setup/channel.runtime-DgMX6VOe.mjs} +5 -5
- package/dist/{monitor-C5cximHM.js → .setup/monitor-JTCxbIQP.mjs} +81 -97
- package/dist/{monitor.webhook-D56OCa3C.js → .setup/monitor.webhook-BwPNGSB4.mjs} +71 -25
- package/dist/{runtime-api-qxdOyk3M.js → .setup/runtime-api-CAA4ZtqF.mjs} +3 -2
- package/dist/{secret-contract-Bxvjh0Hq.js → .setup/secret-contract-Bno_huZF.mjs} +47 -6
- package/dist/.setup/secret-contract-api-CILabB_S.mjs +3 -0
- package/dist/{send-BUvbbedq.js → .setup/send-CNCw5UOt.mjs} +10 -21
- package/dist/{setup-core-LzhuTlsD.js → .setup/setup-core-BLbVCFpT.mjs} +112 -116
- package/dist/{setup-surface-CQMbpZye.js → .setup/setup-surface-gn6fs9pd.mjs} +7 -5
- package/dist/api.js +3 -3
- package/dist/channel-plugin-api.js +1 -1
- package/dist/contract-api.js +1 -1
- package/dist/runtime-api.js +2 -2
- package/dist/secret-contract-api.js +1 -1
- package/dist/setup-api.js +2 -2
- package/dist/setup-entry.js +3 -2
- package/openclaw.plugin.json +6 -0
- package/package.json +5 -46
- package/dist/webhook-spool-DzCPbb3O.js +0 -121
- /package/dist/{group-access-8qHRzDHx.js → .setup/group-access-8qHRzDHx.mjs} +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { n as resolveZaloRuntimeGroupPolicy, t as normalizeZaloAllowEntry } from "./group-access-8qHRzDHx.
|
|
2
|
-
import { t as getZaloRuntime } from "./runtime-api-
|
|
3
|
-
import { c as sendChatAction, d as setWebhook,
|
|
4
|
-
import { createLazyRuntimeModule
|
|
1
|
+
import { n as resolveZaloRuntimeGroupPolicy, t as normalizeZaloAllowEntry } from "./group-access-8qHRzDHx.mjs";
|
|
2
|
+
import { t as getZaloRuntime } from "./runtime-api-CAA4ZtqF.mjs";
|
|
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-CNCw5UOt.mjs";
|
|
4
|
+
import { createLazyRuntimeModule } 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";
|
|
10
11
|
import { registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
11
12
|
import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
12
|
-
import { formatInboundMediaUnavailableText
|
|
13
|
+
import { formatInboundMediaUnavailableText } from "openclaw/plugin-sdk/channel-inbound";
|
|
13
14
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
14
|
-
import {
|
|
15
|
+
import { waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
15
16
|
import { createHostedOutboundMediaStore } from "openclaw/plugin-sdk/outbound-media";
|
|
16
|
-
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
17
17
|
//#region extensions/zalo/src/monitor-durable.ts
|
|
18
18
|
function prepareZaloDurableReplyPayload(params) {
|
|
19
19
|
if (!params.payload.text) return params.payload;
|
|
@@ -30,6 +30,7 @@ function resolveZaloDurableReplyOptions(params) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region extensions/zalo/src/outbound-media.ts
|
|
33
|
+
const ZALO_OUTBOUND_MEDIA_TTL_MS = 12e4;
|
|
33
34
|
const ZALO_OUTBOUND_MEDIA_SEGMENT = "media";
|
|
34
35
|
const ZALO_OUTBOUND_MEDIA_PREFIX = `/${ZALO_OUTBOUND_MEDIA_SEGMENT}/`;
|
|
35
36
|
const ZALO_OUTBOUND_MEDIA_ID_RE = /^[a-f0-9]{24}$/;
|
|
@@ -73,7 +74,7 @@ function resolveHostedZaloMediaRoutePath(params) {
|
|
|
73
74
|
}
|
|
74
75
|
async function prepareHostedZaloMediaUrl(params) {
|
|
75
76
|
const now = asDateTimestampMs(Date.now());
|
|
76
|
-
if ((now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(
|
|
77
|
+
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");
|
|
77
78
|
const routePath = resolveHostedZaloMediaRoutePath({
|
|
78
79
|
webhookUrl: params.webhookUrl,
|
|
79
80
|
webhookPath: params.webhookPath
|
|
@@ -128,7 +129,7 @@ async function tryHandleHostedZaloMediaRequest(req, res) {
|
|
|
128
129
|
res.end("Expired");
|
|
129
130
|
return true;
|
|
130
131
|
}
|
|
131
|
-
if (
|
|
132
|
+
if (url.searchParams.get("token") !== entry.metadata.token) {
|
|
132
133
|
res.statusCode = 401;
|
|
133
134
|
res.end("Unauthorized");
|
|
134
135
|
return true;
|
|
@@ -163,12 +164,7 @@ function resolveZaloTimestampMs(date) {
|
|
|
163
164
|
if (!date) return;
|
|
164
165
|
return date >= UNIX_MILLISECONDS_THRESHOLD ? date : date * 1e3;
|
|
165
166
|
}
|
|
166
|
-
const
|
|
167
|
-
const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-DzCPbb3O.js"), "zaloWebhookIngressRuntime");
|
|
168
|
-
const loadZaloWebhookModule = createLazyRuntimeModule(async () => ({
|
|
169
|
-
...await loadZaloWebhookRuntime(),
|
|
170
|
-
...await loadZaloWebhookIngressRuntime()
|
|
171
|
-
}));
|
|
167
|
+
const loadZaloWebhookModule = createLazyRuntimeModule(() => import("./monitor.webhook-BwPNGSB4.mjs"));
|
|
172
168
|
function releaseSharedHostedMediaRouteRef(routePath) {
|
|
173
169
|
const current = hostedMediaRouteRefs.get(routePath);
|
|
174
170
|
if (!current) return;
|
|
@@ -229,7 +225,22 @@ function logVerbose(core, runtime, message) {
|
|
|
229
225
|
}
|
|
230
226
|
async function handleZaloWebhookRequest(req, res) {
|
|
231
227
|
const { handleZaloWebhookRequest: handleZaloWebhookRequestInternal } = await loadZaloWebhookModule();
|
|
232
|
-
return await handleZaloWebhookRequestInternal(req, res)
|
|
228
|
+
return await handleZaloWebhookRequestInternal(req, res, async ({ update, target }) => {
|
|
229
|
+
await processUpdate({
|
|
230
|
+
update,
|
|
231
|
+
token: target.token,
|
|
232
|
+
account: target.account,
|
|
233
|
+
config: target.config,
|
|
234
|
+
runtime: target.runtime,
|
|
235
|
+
core: target.core,
|
|
236
|
+
mediaMaxMb: target.mediaMaxMb,
|
|
237
|
+
canHostMedia: target.canHostMedia,
|
|
238
|
+
webhookUrl: target.webhookUrl,
|
|
239
|
+
webhookPath: target.webhookPath,
|
|
240
|
+
statusSink: target.statusSink,
|
|
241
|
+
fetcher: target.fetcher
|
|
242
|
+
});
|
|
243
|
+
});
|
|
233
244
|
}
|
|
234
245
|
function startPollingLoop(params) {
|
|
235
246
|
const { token, account, config, runtime, core, mediaMaxMb, canHostMedia, webhookUrl, webhookPath, abortSignal, isStopped, statusSink, fetcher } = params;
|
|
@@ -263,7 +274,9 @@ function startPollingLoop(params) {
|
|
|
263
274
|
} catch (err) {
|
|
264
275
|
if (err instanceof ZaloApiError && err.isPollingTimeout) {} else if (!isStopped() && !abortSignal.aborted) {
|
|
265
276
|
runtime.error?.(`[${account.accountId}] Zalo polling error: ${formatZaloError(err)}`);
|
|
266
|
-
await
|
|
277
|
+
await new Promise((resolve) => {
|
|
278
|
+
setTimeout(resolve, 5e3);
|
|
279
|
+
});
|
|
267
280
|
}
|
|
268
281
|
}
|
|
269
282
|
if (!isStopped() && !abortSignal.aborted) setImmediate(() => {
|
|
@@ -286,8 +299,7 @@ async function processUpdate(params) {
|
|
|
286
299
|
webhookUrl: params.webhookUrl,
|
|
287
300
|
webhookPath: params.webhookPath,
|
|
288
301
|
statusSink,
|
|
289
|
-
fetcher
|
|
290
|
-
turnAdoptionLifecycle: params.turnAdoptionLifecycle
|
|
302
|
+
fetcher
|
|
291
303
|
};
|
|
292
304
|
if (!message) return;
|
|
293
305
|
switch (event_name) {
|
|
@@ -307,9 +319,7 @@ async function processUpdate(params) {
|
|
|
307
319
|
case "message.sticker.received":
|
|
308
320
|
logVerbose(core, runtime, `[${account.accountId}] Received sticker from ${message.from.id}`);
|
|
309
321
|
break;
|
|
310
|
-
case "message.unsupported.received":
|
|
311
|
-
logVerbose(core, runtime, `[${account.accountId}] Received unsupported message type from ${message.from.id}`);
|
|
312
|
-
break;
|
|
322
|
+
case "message.unsupported.received": logVerbose(core, runtime, `[${account.accountId}] Received unsupported message type from ${message.from.id}`);
|
|
313
323
|
}
|
|
314
324
|
}
|
|
315
325
|
async function handleTextMessage(params) {
|
|
@@ -319,7 +329,6 @@ async function handleTextMessage(params) {
|
|
|
319
329
|
await processMessageWithPipeline({
|
|
320
330
|
...params,
|
|
321
331
|
text,
|
|
322
|
-
mediaKind: void 0,
|
|
323
332
|
mediaPath: void 0,
|
|
324
333
|
mediaType: void 0
|
|
325
334
|
});
|
|
@@ -330,8 +339,7 @@ async function handleImageMessage(params) {
|
|
|
330
339
|
const authorization = await authorizeZaloMessage({
|
|
331
340
|
...params,
|
|
332
341
|
text: caption,
|
|
333
|
-
|
|
334
|
-
mediaPath: void 0,
|
|
342
|
+
mediaPath: "__pending_media__",
|
|
335
343
|
mediaType: void 0
|
|
336
344
|
});
|
|
337
345
|
if (!authorization) return;
|
|
@@ -342,7 +350,7 @@ async function handleImageMessage(params) {
|
|
|
342
350
|
const saved = await core.channel.media.saveRemoteMedia({
|
|
343
351
|
url: photo_url,
|
|
344
352
|
maxBytes,
|
|
345
|
-
|
|
353
|
+
timeoutMs: ZALO_MEDIA_RESPONSE_HEADER_TIMEOUT_MS,
|
|
346
354
|
readIdleTimeoutMs: ZALO_MEDIA_READ_IDLE_TIMEOUT_MS
|
|
347
355
|
});
|
|
348
356
|
mediaPath = saved.path;
|
|
@@ -351,21 +359,21 @@ async function handleImageMessage(params) {
|
|
|
351
359
|
runtime.error?.(`[${account.accountId}] Failed to download Zalo image: ${String(err)}`);
|
|
352
360
|
}
|
|
353
361
|
const agentBody = mediaPath ? authorization.rawBody : formatInboundMediaUnavailableText({
|
|
354
|
-
body:
|
|
362
|
+
body: caption,
|
|
363
|
+
mediaPlaceholder: "<media:image>",
|
|
355
364
|
notice: "[zalo image attachment unavailable]"
|
|
356
365
|
});
|
|
357
366
|
await processMessageWithPipeline({
|
|
358
367
|
...params,
|
|
359
368
|
authorization,
|
|
360
369
|
agentBody,
|
|
361
|
-
mediaKind: "image",
|
|
362
370
|
text: caption,
|
|
363
371
|
mediaPath,
|
|
364
372
|
mediaType
|
|
365
373
|
});
|
|
366
374
|
}
|
|
367
375
|
async function authorizeZaloMessage(params) {
|
|
368
|
-
const { message, account, config, runtime, core, text, token, statusSink, fetcher } = params;
|
|
376
|
+
const { message, account, config, runtime, core, text, mediaPath, token, statusSink, fetcher } = params;
|
|
369
377
|
const pairing = createChannelPairingController({
|
|
370
378
|
core,
|
|
371
379
|
channel: "zalo",
|
|
@@ -378,7 +386,7 @@ async function authorizeZaloMessage(params) {
|
|
|
378
386
|
const senderName = from.display_name ?? from.name;
|
|
379
387
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
380
388
|
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
381
|
-
const rawBody = text?.trim()
|
|
389
|
+
const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
|
|
382
390
|
const { groupPolicy, providerMissingFallbackApplied } = resolveZaloRuntimeGroupPolicy({
|
|
383
391
|
providerConfigPresent: config.channels?.zalo !== void 0,
|
|
384
392
|
groupPolicy: account.config.groupPolicy,
|
|
@@ -460,7 +468,7 @@ async function authorizeZaloMessage(params) {
|
|
|
460
468
|
};
|
|
461
469
|
}
|
|
462
470
|
async function processMessageWithPipeline(params) {
|
|
463
|
-
const { message, token, account, config, runtime, core, mediaPath,
|
|
471
|
+
const { message, token, account, config, runtime, core, mediaPath, mediaType, statusSink, fetcher, agentBody: agentBodyOverride, authorization: authorizationOverride } = params;
|
|
464
472
|
const { message_id, date } = message;
|
|
465
473
|
const authorization = authorizationOverride ?? await authorizeZaloMessage({
|
|
466
474
|
...params,
|
|
@@ -470,14 +478,16 @@ async function processMessageWithPipeline(params) {
|
|
|
470
478
|
if (!authorization) return;
|
|
471
479
|
const { isGroup, chatId, senderId, senderName, rawBody, commandAuthorized } = authorization;
|
|
472
480
|
const agentBody = agentBodyOverride ?? rawBody;
|
|
473
|
-
const { route, buildEnvelope } =
|
|
481
|
+
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
474
482
|
cfg: config,
|
|
475
483
|
channel: "zalo",
|
|
476
484
|
accountId: account.accountId,
|
|
477
485
|
peer: {
|
|
478
486
|
kind: isGroup ? "group" : "direct",
|
|
479
487
|
id: chatId
|
|
480
|
-
}
|
|
488
|
+
},
|
|
489
|
+
runtime: core.channel,
|
|
490
|
+
sessionStore: config.session?.store
|
|
481
491
|
});
|
|
482
492
|
if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
|
|
483
493
|
logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`);
|
|
@@ -485,7 +495,7 @@ async function processMessageWithPipeline(params) {
|
|
|
485
495
|
}
|
|
486
496
|
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
|
487
497
|
const timestamp = resolveZaloTimestampMs(date);
|
|
488
|
-
const body = buildEnvelope({
|
|
498
|
+
const { storePath, body } = buildEnvelope({
|
|
489
499
|
channel: "Zalo",
|
|
490
500
|
from: fromLabel,
|
|
491
501
|
timestamp,
|
|
@@ -508,7 +518,6 @@ async function processMessageWithPipeline(params) {
|
|
|
508
518
|
},
|
|
509
519
|
route: {
|
|
510
520
|
agentId: route.agentId,
|
|
511
|
-
dmScope: route.dmScope,
|
|
512
521
|
accountId: route.accountId,
|
|
513
522
|
routeSessionKey: route.sessionKey
|
|
514
523
|
},
|
|
@@ -519,11 +528,10 @@ async function processMessageWithPipeline(params) {
|
|
|
519
528
|
rawBody,
|
|
520
529
|
commandBody: rawBody
|
|
521
530
|
},
|
|
522
|
-
media:
|
|
531
|
+
media: mediaPath || mediaType ? [{
|
|
523
532
|
path: mediaPath,
|
|
524
533
|
url: mediaPath,
|
|
525
|
-
contentType: mediaType
|
|
526
|
-
kind: mediaKind ?? void 0
|
|
534
|
+
contentType: mediaType
|
|
527
535
|
}] : void 0,
|
|
528
536
|
extra: {
|
|
529
537
|
CommandAuthorized: commandAuthorized,
|
|
@@ -535,16 +543,16 @@ async function processMessageWithPipeline(params) {
|
|
|
535
543
|
channel: "zalo",
|
|
536
544
|
accountId: account.accountId
|
|
537
545
|
});
|
|
538
|
-
await core.channel.inbound.
|
|
546
|
+
await core.channel.inbound.dispatchReply({
|
|
539
547
|
cfg: config,
|
|
540
548
|
channel: "zalo",
|
|
541
549
|
accountId: account.accountId,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
sessionKey: route.sessionKey
|
|
546
|
-
},
|
|
550
|
+
agentId: route.agentId,
|
|
551
|
+
routeSessionKey: route.sessionKey,
|
|
552
|
+
storePath,
|
|
547
553
|
ctxPayload,
|
|
554
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
555
|
+
dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
548
556
|
delivery: {
|
|
549
557
|
preparePayload: (payload) => prepareZaloDurableReplyPayload({
|
|
550
558
|
payload,
|
|
@@ -601,8 +609,7 @@ async function processMessageWithPipeline(params) {
|
|
|
601
609
|
} },
|
|
602
610
|
record: { onRecordError: (err) => {
|
|
603
611
|
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
|
|
604
|
-
} }
|
|
605
|
-
...params.turnAdoptionLifecycle ? { turnAdoptionLifecycle: params.turnAdoptionLifecycle } : {}
|
|
612
|
+
} }
|
|
606
613
|
});
|
|
607
614
|
}
|
|
608
615
|
async function deliverZaloReply(params) {
|
|
@@ -664,7 +671,6 @@ async function monitorZaloProvider(options) {
|
|
|
664
671
|
}) : void 0;
|
|
665
672
|
let stopped = false;
|
|
666
673
|
const stopHandlers = [];
|
|
667
|
-
const asyncStopHandlers = [];
|
|
668
674
|
let cleanupWebhook;
|
|
669
675
|
const stop = () => {
|
|
670
676
|
if (stopped) return;
|
|
@@ -674,10 +680,6 @@ async function monitorZaloProvider(options) {
|
|
|
674
680
|
const stopOnAbort = () => {
|
|
675
681
|
if (!useWebhook) stop();
|
|
676
682
|
};
|
|
677
|
-
if (abortSignal.aborted) {
|
|
678
|
-
stopOnAbort();
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
683
|
abortSignal.addEventListener("abort", stopOnAbort, { once: true });
|
|
682
684
|
runtime.log?.(`[${account.accountId}] Zalo provider init mode=${mode} mediaMaxMb=${String(effectiveMediaMaxMb)}`);
|
|
683
685
|
try {
|
|
@@ -690,44 +692,46 @@ async function monitorZaloProvider(options) {
|
|
|
690
692
|
stopHandlers.push(unregisterHostedMediaRoute);
|
|
691
693
|
}
|
|
692
694
|
if (useWebhook) {
|
|
693
|
-
const {
|
|
695
|
+
const { registerZaloWebhookTarget } = await loadZaloWebhookModule();
|
|
694
696
|
if (!effectiveWebhookUrl || !webhookSecret) throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
|
|
695
697
|
if (!effectiveWebhookUrl.startsWith("https://")) throw new Error("Zalo webhook URL must use HTTPS");
|
|
696
698
|
if (webhookSecret.length < 8 || webhookSecret.length > 256) throw new Error("Zalo webhook secret must be 8-256 characters");
|
|
697
699
|
const path = effectiveWebhookPath;
|
|
698
700
|
if (!path) throw new Error("Zalo webhookPath could not be derived");
|
|
699
701
|
runtime.log?.(`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(effectiveWebhookUrl)}`);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
runtime
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
ingress.start();
|
|
723
|
-
asyncStopHandlers.push(ingress.stop);
|
|
702
|
+
await setWebhook(token, {
|
|
703
|
+
url: effectiveWebhookUrl,
|
|
704
|
+
secret_token: webhookSecret
|
|
705
|
+
}, fetcher);
|
|
706
|
+
let webhookCleanupPromise;
|
|
707
|
+
cleanupWebhook = async () => {
|
|
708
|
+
if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
|
|
709
|
+
runtime.log?.(`[${account.accountId}] Zalo stopping; deleting webhook`);
|
|
710
|
+
try {
|
|
711
|
+
await deleteWebhook(token, fetcher, WEBHOOK_CLEANUP_TIMEOUT_MS);
|
|
712
|
+
runtime.log?.(`[${account.accountId}] Zalo webhook deleted`);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const detail = err instanceof Error && err.name === "AbortError" ? `timed out after ${String(WEBHOOK_CLEANUP_TIMEOUT_MS)}ms` : formatZaloError(err);
|
|
715
|
+
runtime.error?.(`[${account.accountId}] Zalo webhook delete failed: ${detail}`);
|
|
716
|
+
}
|
|
717
|
+
})();
|
|
718
|
+
await webhookCleanupPromise;
|
|
719
|
+
};
|
|
720
|
+
runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
|
|
724
721
|
const unregister = registerZaloWebhookTarget({
|
|
722
|
+
token,
|
|
725
723
|
account,
|
|
726
724
|
config,
|
|
727
725
|
runtime,
|
|
726
|
+
core,
|
|
728
727
|
path,
|
|
728
|
+
webhookUrl: effectiveWebhookUrl,
|
|
729
|
+
webhookPath: path,
|
|
729
730
|
secret: webhookSecret,
|
|
730
|
-
|
|
731
|
+
statusSink: (patch) => statusSink?.(patch),
|
|
732
|
+
mediaMaxMb: effectiveMediaMaxMb,
|
|
733
|
+
canHostMedia,
|
|
734
|
+
fetcher
|
|
731
735
|
}, { route: {
|
|
732
736
|
auth: "plugin",
|
|
733
737
|
match: "exact",
|
|
@@ -744,25 +748,6 @@ async function monitorZaloProvider(options) {
|
|
|
744
748
|
}
|
|
745
749
|
} });
|
|
746
750
|
stopHandlers.push(unregister);
|
|
747
|
-
await setWebhook(token, {
|
|
748
|
-
url: effectiveWebhookUrl,
|
|
749
|
-
secret_token: webhookSecret
|
|
750
|
-
}, fetcher);
|
|
751
|
-
let webhookCleanupPromise;
|
|
752
|
-
cleanupWebhook = async () => {
|
|
753
|
-
if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
|
|
754
|
-
runtime.log?.(`[${account.accountId}] Zalo stopping; deleting webhook`);
|
|
755
|
-
try {
|
|
756
|
-
await deleteWebhook(token, fetcher, WEBHOOK_CLEANUP_TIMEOUT_MS);
|
|
757
|
-
runtime.log?.(`[${account.accountId}] Zalo webhook deleted`);
|
|
758
|
-
} catch (err) {
|
|
759
|
-
const detail = err instanceof Error && err.name === "AbortError" ? `timed out after ${String(WEBHOOK_CLEANUP_TIMEOUT_MS)}ms` : formatZaloError(err);
|
|
760
|
-
runtime.error?.(`[${account.accountId}] Zalo webhook delete failed: ${detail}`);
|
|
761
|
-
}
|
|
762
|
-
})();
|
|
763
|
-
await webhookCleanupPromise;
|
|
764
|
-
};
|
|
765
|
-
runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
|
|
766
751
|
await waitForAbortSignal(abortSignal);
|
|
767
752
|
return;
|
|
768
753
|
}
|
|
@@ -806,7 +791,6 @@ async function monitorZaloProvider(options) {
|
|
|
806
791
|
abortSignal.removeEventListener("abort", stopOnAbort);
|
|
807
792
|
await cleanupWebhook?.();
|
|
808
793
|
stop();
|
|
809
|
-
for (const stopAsync of asyncStopHandlers) await stopAsync();
|
|
810
794
|
runtime.log?.(`[${account.accountId}] Zalo provider stopped mode=${mode}`);
|
|
811
795
|
}
|
|
812
796
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { B as registerWebhookTargetWithPluginRoute,
|
|
2
|
-
import {
|
|
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.mjs";
|
|
2
|
+
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
3
3
|
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
4
|
-
import { readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
5
4
|
//#region extensions/zalo/src/monitor.webhook.ts
|
|
6
|
-
const
|
|
7
|
-
const ZALO_WEBHOOK_ACCEPTED_VALUE = "durable";
|
|
5
|
+
const ZALO_WEBHOOK_REPLAY_WINDOW_MS = 3e5;
|
|
8
6
|
const webhookTargets = /* @__PURE__ */ new Map();
|
|
9
7
|
const webhookRateLimiter = createFixedWindowRateLimiter({
|
|
10
8
|
windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
|
|
11
9
|
maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
|
|
12
10
|
maxTrackedKeys: WEBHOOK_RATE_LIMIT_DEFAULTS.maxTrackedKeys
|
|
13
11
|
});
|
|
12
|
+
const recentWebhookEvents = createClaimableDedupe({
|
|
13
|
+
ttlMs: ZALO_WEBHOOK_REPLAY_WINDOW_MS,
|
|
14
|
+
memoryMaxSize: 5e3
|
|
15
|
+
});
|
|
14
16
|
const webhookAnomalyTracker = createWebhookAnomalyTracker({
|
|
15
17
|
maxTrackedKeys: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.maxTrackedKeys,
|
|
16
18
|
ttlMs: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.ttlMs,
|
|
@@ -18,6 +20,7 @@ const webhookAnomalyTracker = createWebhookAnomalyTracker({
|
|
|
18
20
|
});
|
|
19
21
|
function clearZaloWebhookSecurityStateForTest() {
|
|
20
22
|
webhookRateLimiter.clear();
|
|
23
|
+
recentWebhookEvents.clearMemory();
|
|
21
24
|
webhookAnomalyTracker.clear();
|
|
22
25
|
}
|
|
23
26
|
function getZaloWebhookRateLimitStateSizeForTest() {
|
|
@@ -26,6 +29,47 @@ function getZaloWebhookRateLimitStateSizeForTest() {
|
|
|
26
29
|
function getZaloWebhookStatusCounterSizeForTest() {
|
|
27
30
|
return webhookAnomalyTracker.size();
|
|
28
31
|
}
|
|
32
|
+
function buildReplayEventCacheKey(target, update) {
|
|
33
|
+
const messageId = update.message?.message_id;
|
|
34
|
+
if (!messageId) return null;
|
|
35
|
+
const chatId = update.message?.chat?.id ?? "";
|
|
36
|
+
const senderId = update.message?.from?.id ?? "";
|
|
37
|
+
return JSON.stringify([
|
|
38
|
+
target.path,
|
|
39
|
+
target.account.accountId,
|
|
40
|
+
update.event_name,
|
|
41
|
+
chatId,
|
|
42
|
+
senderId,
|
|
43
|
+
messageId
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
var ZaloRetryableWebhookError = class extends Error {
|
|
47
|
+
constructor(message, options) {
|
|
48
|
+
super(message, options);
|
|
49
|
+
this.name = "ZaloRetryableWebhookError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
async function processZaloReplayGuardedUpdate(params) {
|
|
53
|
+
const replayEventKey = buildReplayEventCacheKey(params.target, params.update);
|
|
54
|
+
if (replayEventKey) {
|
|
55
|
+
if ((await recentWebhookEvents.claim(replayEventKey, { now: params.nowMs })).kind !== "claimed") return "duplicate";
|
|
56
|
+
}
|
|
57
|
+
params.target.statusSink?.({ lastInboundAt: Date.now() });
|
|
58
|
+
try {
|
|
59
|
+
await params.processUpdate({
|
|
60
|
+
update: params.update,
|
|
61
|
+
target: params.target
|
|
62
|
+
});
|
|
63
|
+
if (replayEventKey) await recentWebhookEvents.commit(replayEventKey);
|
|
64
|
+
return "processed";
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (replayEventKey) {
|
|
67
|
+
if (error instanceof ZaloRetryableWebhookError) recentWebhookEvents.release(replayEventKey, { error });
|
|
68
|
+
else await recentWebhookEvents.commit(replayEventKey);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
29
73
|
function recordWebhookStatus(runtime, path, statusCode) {
|
|
30
74
|
webhookAnomalyTracker.record({
|
|
31
75
|
key: `${path}:${statusCode}`,
|
|
@@ -46,7 +90,7 @@ function registerZaloWebhookTarget(target, opts) {
|
|
|
46
90
|
}).unregister;
|
|
47
91
|
return registerWebhookTarget(webhookTargets, target, opts).unregister;
|
|
48
92
|
}
|
|
49
|
-
async function handleZaloWebhookRequest(req, res) {
|
|
93
|
+
async function handleZaloWebhookRequest(req, res, processUpdate) {
|
|
50
94
|
return await withResolvedWebhookRequestPipeline({
|
|
51
95
|
req,
|
|
52
96
|
res,
|
|
@@ -62,12 +106,13 @@ async function handleZaloWebhookRequest(req, res) {
|
|
|
62
106
|
trustedProxies,
|
|
63
107
|
allowRealIpFallback
|
|
64
108
|
}) ?? req.socket.remoteAddress ?? "unknown"}`;
|
|
109
|
+
const nowMs = Date.now();
|
|
65
110
|
if (!applyBasicWebhookRequestGuards({
|
|
66
111
|
req,
|
|
67
112
|
res,
|
|
68
113
|
rateLimiter: webhookRateLimiter,
|
|
69
114
|
rateLimitKey,
|
|
70
|
-
nowMs
|
|
115
|
+
nowMs
|
|
71
116
|
})) {
|
|
72
117
|
recordWebhookStatus(targets[0]?.runtime, path, res.statusCode);
|
|
73
118
|
return true;
|
|
@@ -90,39 +135,40 @@ async function handleZaloWebhookRequest(req, res) {
|
|
|
90
135
|
recordWebhookStatus(target.runtime, path, res.statusCode);
|
|
91
136
|
return true;
|
|
92
137
|
}
|
|
93
|
-
const body = await
|
|
138
|
+
const body = await readJsonWebhookBodyOrReject({
|
|
94
139
|
req,
|
|
95
140
|
res,
|
|
96
|
-
maxBytes:
|
|
141
|
+
maxBytes: 1048576,
|
|
97
142
|
timeoutMs: 3e4,
|
|
98
|
-
|
|
143
|
+
emptyObjectOnEmpty: false,
|
|
144
|
+
invalidJsonMessage: "Bad Request"
|
|
99
145
|
});
|
|
100
146
|
if (!body.ok) {
|
|
101
147
|
recordWebhookStatus(target.runtime, path, res.statusCode);
|
|
102
148
|
return true;
|
|
103
149
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
res.
|
|
150
|
+
const raw = body.value;
|
|
151
|
+
const record = raw && typeof raw === "object" ? raw : null;
|
|
152
|
+
const update = record && record.ok === true && record.result ? record.result : record ?? void 0;
|
|
153
|
+
if (!update?.event_name) {
|
|
154
|
+
res.statusCode = 400;
|
|
155
|
+
res.end("Bad Request");
|
|
109
156
|
recordWebhookStatus(target.runtime, path, res.statusCode);
|
|
110
|
-
target.runtime.error?.(`[${target.account.accountId}] Zalo webhook admission failed: ${String(error)}`);
|
|
111
157
|
return true;
|
|
112
158
|
}
|
|
113
|
-
|
|
159
|
+
processZaloReplayGuardedUpdate({
|
|
160
|
+
target,
|
|
161
|
+
update,
|
|
162
|
+
processUpdate,
|
|
163
|
+
nowMs
|
|
164
|
+
}).catch((err) => {
|
|
165
|
+
target.runtime.error?.(`[${target.account.accountId}] Zalo webhook failed: ${String(err)}`);
|
|
166
|
+
});
|
|
114
167
|
res.statusCode = 200;
|
|
115
168
|
res.end("ok");
|
|
116
169
|
return true;
|
|
117
170
|
}
|
|
118
171
|
});
|
|
119
172
|
}
|
|
120
|
-
const zaloWebhookRuntime = {
|
|
121
|
-
clearZaloWebhookSecurityStateForTest,
|
|
122
|
-
getZaloWebhookRateLimitStateSizeForTest,
|
|
123
|
-
getZaloWebhookStatusCounterSizeForTest,
|
|
124
|
-
handleZaloWebhookRequest,
|
|
125
|
-
registerZaloWebhookTarget
|
|
126
|
-
};
|
|
127
173
|
//#endregion
|
|
128
|
-
export {
|
|
174
|
+
export { ZaloRetryableWebhookError, clearZaloWebhookSecurityStateForTest, getZaloWebhookRateLimitStateSizeForTest, getZaloWebhookStatusCounterSizeForTest, handleZaloWebhookRequest, registerZaloWebhookTarget };
|
|
@@ -6,10 +6,11 @@ import { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpe
|
|
|
6
6
|
import { chunkTextForOutbound as chunkTextForOutbound$1 } from "openclaw/plugin-sdk/text-chunking";
|
|
7
7
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, buildChannelConfigSchema, createDedupeCache, formatPairingApproveHint, jsonResult, normalizeAccountId as normalizeAccountId$1, readStringParam, resolveClientIp } from "openclaw/plugin-sdk/core";
|
|
8
8
|
import { buildSecretInputSchema, hasConfiguredSecretInput as hasConfiguredSecretInput$1, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
9
|
-
import { addWildcardAllowFrom, applyAccountNameToChannelSection, applySetupAccountConfigPatch, buildSingleChannelSecretPromptState as buildSingleChannelSecretPromptState$1, mergeAllowFromEntries as mergeAllowFromEntries$1, migrateBaseNameToDefaultAccount, promptSingleChannelSecretInput as promptSingleChannelSecretInput$1, runSingleChannelSecretStep as runSingleChannelSecretStep$1, setTopLevelChannelDmPolicyWithAllowFrom } from "openclaw/plugin-sdk/setup";
|
|
9
|
+
import { addWildcardAllowFrom as addWildcardAllowFrom$1, applyAccountNameToChannelSection, applySetupAccountConfigPatch, buildSingleChannelSecretPromptState as buildSingleChannelSecretPromptState$1, mergeAllowFromEntries as mergeAllowFromEntries$1, migrateBaseNameToDefaultAccount, promptSingleChannelSecretInput as promptSingleChannelSecretInput$1, runSingleChannelSecretStep as runSingleChannelSecretStep$1, setTopLevelChannelDmPolicyWithAllowFrom } from "openclaw/plugin-sdk/setup";
|
|
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";
|
|
13
14
|
import { waitForAbortSignal } from "openclaw/plugin-sdk/runtime";
|
|
14
15
|
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";
|
|
15
16
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
@@ -19,4 +20,4 @@ const { setRuntime: setZaloRuntime, getRuntime: getZaloRuntime } = createPluginR
|
|
|
19
20
|
errorMessage: "Zalo runtime not initialized"
|
|
20
21
|
});
|
|
21
22
|
//#endregion
|
|
22
|
-
export { mergeAllowFromEntries$1 as A, registerWebhookTargetWithPluginRoute as B, formatAllowFromLowercase$1 as C, isNumericTargetId as D, isNormalizedSenderAllowed as E, promptSingleChannelSecretInput$1 as F,
|
|
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 };
|
|
@@ -1,10 +1,51 @@
|
|
|
1
|
-
import { collectConditionalChannelFieldAssignments,
|
|
1
|
+
import { collectConditionalChannelFieldAssignments, getChannelSurface, hasOwnProperty } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
2
2
|
//#region extensions/zalo/src/secret-contract.ts
|
|
3
|
-
const secretTargetRegistryEntries =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const secretTargetRegistryEntries = [
|
|
4
|
+
{
|
|
5
|
+
id: "channels.zalo.accounts.*.botToken",
|
|
6
|
+
targetType: "channels.zalo.accounts.*.botToken",
|
|
7
|
+
configFile: "openclaw.json",
|
|
8
|
+
pathPattern: "channels.zalo.accounts.*.botToken",
|
|
9
|
+
secretShape: "secret_input",
|
|
10
|
+
expectedResolvedValue: "string",
|
|
11
|
+
includeInPlan: true,
|
|
12
|
+
includeInConfigure: true,
|
|
13
|
+
includeInAudit: true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "channels.zalo.accounts.*.webhookSecret",
|
|
17
|
+
targetType: "channels.zalo.accounts.*.webhookSecret",
|
|
18
|
+
configFile: "openclaw.json",
|
|
19
|
+
pathPattern: "channels.zalo.accounts.*.webhookSecret",
|
|
20
|
+
secretShape: "secret_input",
|
|
21
|
+
expectedResolvedValue: "string",
|
|
22
|
+
includeInPlan: true,
|
|
23
|
+
includeInConfigure: true,
|
|
24
|
+
includeInAudit: true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "channels.zalo.botToken",
|
|
28
|
+
targetType: "channels.zalo.botToken",
|
|
29
|
+
configFile: "openclaw.json",
|
|
30
|
+
pathPattern: "channels.zalo.botToken",
|
|
31
|
+
secretShape: "secret_input",
|
|
32
|
+
expectedResolvedValue: "string",
|
|
33
|
+
includeInPlan: true,
|
|
34
|
+
includeInConfigure: true,
|
|
35
|
+
includeInAudit: true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "channels.zalo.webhookSecret",
|
|
39
|
+
targetType: "channels.zalo.webhookSecret",
|
|
40
|
+
configFile: "openclaw.json",
|
|
41
|
+
pathPattern: "channels.zalo.webhookSecret",
|
|
42
|
+
secretShape: "secret_input",
|
|
43
|
+
expectedResolvedValue: "string",
|
|
44
|
+
includeInPlan: true,
|
|
45
|
+
includeInConfigure: true,
|
|
46
|
+
includeInAudit: true
|
|
47
|
+
}
|
|
48
|
+
];
|
|
8
49
|
function collectRuntimeConfigAssignments(params) {
|
|
9
50
|
const resolved = getChannelSurface(params.config, "zalo");
|
|
10
51
|
if (!resolved) return;
|