@openclaw/zalo 2026.2.25 → 2026.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.2
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.3.1
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
15
+ ## 2026.2.26
16
+
17
+ ### Changes
18
+
19
+ - Version alignment with core OpenClaw release numbers.
20
+
3
21
  ## 2026.2.25
4
22
 
5
23
  ### Changes
package/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
3
  import { zaloDock, zaloPlugin } from "./src/channel.js";
4
- import { handleZaloWebhookRequest } from "./src/monitor.js";
5
4
  import { setZaloRuntime } from "./src/runtime.js";
6
5
 
7
6
  const plugin = {
@@ -12,7 +11,6 @@ const plugin = {
12
11
  register(api: OpenClawPluginApi) {
13
12
  setZaloRuntime(api.runtime);
14
13
  api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
15
- api.registerHttpHandler(handleZaloWebhookRequest);
16
14
  },
17
15
  };
18
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/zalo",
3
- "version": "2026.2.25",
3
+ "version": "2026.3.2",
4
4
  "description": "OpenClaw Zalo channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/accounts.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import {
3
+ DEFAULT_ACCOUNT_ID,
4
+ normalizeAccountId,
5
+ normalizeOptionalAccountId,
6
+ } from "openclaw/plugin-sdk/account-id";
3
7
  import { resolveZaloToken } from "./token.js";
4
8
  import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
5
9
 
@@ -23,8 +27,12 @@ export function listZaloAccountIds(cfg: OpenClawConfig): string[] {
23
27
 
24
28
  export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string {
25
29
  const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
26
- if (zaloConfig?.defaultAccount?.trim()) {
27
- return zaloConfig.defaultAccount.trim();
30
+ const preferred = normalizeOptionalAccountId(zaloConfig?.defaultAccount);
31
+ if (
32
+ preferred &&
33
+ listZaloAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
34
+ ) {
35
+ return preferred;
28
36
  }
29
37
  const ids = listZaloAccountIds(cfg);
30
38
  if (ids.includes(DEFAULT_ACCOUNT_ID)) {
@@ -54,6 +62,7 @@ function mergeZaloAccountConfig(cfg: OpenClawConfig, accountId: string): ZaloAcc
54
62
  export function resolveZaloAccount(params: {
55
63
  cfg: OpenClawConfig;
56
64
  accountId?: string | null;
65
+ allowUnresolvedSecretRef?: boolean;
57
66
  }): ResolvedZaloAccount {
58
67
  const accountId = normalizeAccountId(params.accountId);
59
68
  const baseEnabled = (params.cfg.channels?.zalo as ZaloConfig | undefined)?.enabled !== false;
@@ -63,6 +72,7 @@ export function resolveZaloAccount(params: {
63
72
  const tokenResolution = resolveZaloToken(
64
73
  params.cfg.channels?.zalo as ZaloConfig | undefined,
65
74
  accountId,
75
+ { allowUnresolvedSecretRef: params.allowUnresolvedSecretRef },
66
76
  );
67
77
 
68
78
  return {
@@ -0,0 +1,102 @@
1
+ import type { ReplyPayload } from "openclaw/plugin-sdk";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { zaloPlugin } from "./channel.js";
4
+
5
+ vi.mock("./send.js", () => ({
6
+ sendMessageZalo: vi.fn().mockResolvedValue({ ok: true, messageId: "zl-1" }),
7
+ }));
8
+
9
+ function baseCtx(payload: ReplyPayload) {
10
+ return {
11
+ cfg: {},
12
+ to: "123456789",
13
+ text: "",
14
+ payload,
15
+ };
16
+ }
17
+
18
+ describe("zaloPlugin outbound sendPayload", () => {
19
+ let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalo"]>>;
20
+
21
+ beforeEach(async () => {
22
+ const mod = await import("./send.js");
23
+ mockedSend = vi.mocked(mod.sendMessageZalo);
24
+ mockedSend.mockClear();
25
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zl-1" });
26
+ });
27
+
28
+ it("text-only delegates to sendText", async () => {
29
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zl-t1" });
30
+
31
+ const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({ text: "hello" }));
32
+
33
+ expect(mockedSend).toHaveBeenCalledWith("123456789", "hello", expect.any(Object));
34
+ expect(result).toMatchObject({ channel: "zalo", messageId: "zl-t1" });
35
+ });
36
+
37
+ it("single media delegates to sendMedia", async () => {
38
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zl-m1" });
39
+
40
+ const result = await zaloPlugin.outbound!.sendPayload!(
41
+ baseCtx({ text: "cap", mediaUrl: "https://example.com/a.jpg" }),
42
+ );
43
+
44
+ expect(mockedSend).toHaveBeenCalledWith(
45
+ "123456789",
46
+ "cap",
47
+ expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }),
48
+ );
49
+ expect(result).toMatchObject({ channel: "zalo" });
50
+ });
51
+
52
+ it("multi-media iterates URLs with caption on first", async () => {
53
+ mockedSend
54
+ .mockResolvedValueOnce({ ok: true, messageId: "zl-1" })
55
+ .mockResolvedValueOnce({ ok: true, messageId: "zl-2" });
56
+
57
+ const result = await zaloPlugin.outbound!.sendPayload!(
58
+ baseCtx({
59
+ text: "caption",
60
+ mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
61
+ }),
62
+ );
63
+
64
+ expect(mockedSend).toHaveBeenCalledTimes(2);
65
+ expect(mockedSend).toHaveBeenNthCalledWith(
66
+ 1,
67
+ "123456789",
68
+ "caption",
69
+ expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }),
70
+ );
71
+ expect(mockedSend).toHaveBeenNthCalledWith(
72
+ 2,
73
+ "123456789",
74
+ "",
75
+ expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }),
76
+ );
77
+ expect(result).toMatchObject({ channel: "zalo", messageId: "zl-2" });
78
+ });
79
+
80
+ it("empty payload returns no-op", async () => {
81
+ const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({}));
82
+
83
+ expect(mockedSend).not.toHaveBeenCalled();
84
+ expect(result).toEqual({ channel: "zalo", messageId: "" });
85
+ });
86
+
87
+ it("chunking splits long text", async () => {
88
+ mockedSend
89
+ .mockResolvedValueOnce({ ok: true, messageId: "zl-c1" })
90
+ .mockResolvedValueOnce({ ok: true, messageId: "zl-c2" });
91
+
92
+ const longText = "a".repeat(3000);
93
+ const result = await zaloPlugin.outbound!.sendPayload!(baseCtx({ text: longText }));
94
+
95
+ // textChunkLimit is 2000 with chunkTextForOutbound, so it should split
96
+ expect(mockedSend.mock.calls.length).toBeGreaterThanOrEqual(2);
97
+ for (const call of mockedSend.mock.calls) {
98
+ expect((call[1] as string).length).toBeLessThanOrEqual(2000);
99
+ }
100
+ expect(result).toMatchObject({ channel: "zalo" });
101
+ });
102
+ });
package/src/channel.ts CHANGED
@@ -32,6 +32,7 @@ import { ZaloConfigSchema } from "./config-schema.js";
32
32
  import { zaloOnboardingAdapter } from "./onboarding.js";
33
33
  import { probeZalo } from "./probe.js";
34
34
  import { resolveZaloProxyFetch } from "./proxy.js";
35
+ import { normalizeSecretInputString } from "./secret-input.js";
35
36
  import { sendMessageZalo } from "./send.js";
36
37
  import { collectZaloStatusIssues } from "./status-issues.js";
37
38
 
@@ -302,6 +303,40 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
302
303
  chunker: chunkTextForOutbound,
303
304
  chunkerMode: "text",
304
305
  textChunkLimit: 2000,
306
+ sendPayload: async (ctx) => {
307
+ const text = ctx.payload.text ?? "";
308
+ const urls = ctx.payload.mediaUrls?.length
309
+ ? ctx.payload.mediaUrls
310
+ : ctx.payload.mediaUrl
311
+ ? [ctx.payload.mediaUrl]
312
+ : [];
313
+ if (!text && urls.length === 0) {
314
+ return { channel: "zalo", messageId: "" };
315
+ }
316
+ if (urls.length > 0) {
317
+ let lastResult = await zaloPlugin.outbound!.sendMedia!({
318
+ ...ctx,
319
+ text,
320
+ mediaUrl: urls[0],
321
+ });
322
+ for (let i = 1; i < urls.length; i++) {
323
+ lastResult = await zaloPlugin.outbound!.sendMedia!({
324
+ ...ctx,
325
+ text: "",
326
+ mediaUrl: urls[i],
327
+ });
328
+ }
329
+ return lastResult;
330
+ }
331
+ const outbound = zaloPlugin.outbound!;
332
+ const limit = outbound.textChunkLimit;
333
+ const chunks = limit && outbound.chunker ? outbound.chunker(text, limit) : [text];
334
+ let lastResult: Awaited<ReturnType<NonNullable<typeof outbound.sendText>>>;
335
+ for (const chunk of chunks) {
336
+ lastResult = await outbound.sendText!({ ...ctx, text: chunk });
337
+ }
338
+ return lastResult!;
339
+ },
305
340
  sendText: async ({ to, text, accountId, cfg }) => {
306
341
  const result = await sendMessageZalo(to, text, {
307
342
  accountId: accountId ?? undefined,
@@ -388,7 +423,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
388
423
  abortSignal: ctx.abortSignal,
389
424
  useWebhook: Boolean(account.config.webhookUrl),
390
425
  webhookUrl: account.config.webhookUrl,
391
- webhookSecret: account.config.webhookSecret,
426
+ webhookSecret: normalizeSecretInputString(account.config.webhookSecret),
392
427
  webhookPath: account.config.webhookPath,
393
428
  fetcher,
394
429
  statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ZaloConfigSchema } from "./config-schema.js";
3
+
4
+ describe("ZaloConfigSchema SecretInput", () => {
5
+ it("accepts SecretRef botToken and webhookSecret at top-level", () => {
6
+ const result = ZaloConfigSchema.safeParse({
7
+ botToken: { source: "env", provider: "default", id: "ZALO_BOT_TOKEN" },
8
+ webhookUrl: "https://example.com/zalo",
9
+ webhookSecret: { source: "env", provider: "default", id: "ZALO_WEBHOOK_SECRET" },
10
+ });
11
+ expect(result.success).toBe(true);
12
+ });
13
+
14
+ it("accepts SecretRef botToken and webhookSecret on account", () => {
15
+ const result = ZaloConfigSchema.safeParse({
16
+ accounts: {
17
+ work: {
18
+ botToken: { source: "env", provider: "default", id: "ZALO_WORK_BOT_TOKEN" },
19
+ webhookUrl: "https://example.com/zalo/work",
20
+ webhookSecret: {
21
+ source: "env",
22
+ provider: "default",
23
+ id: "ZALO_WORK_WEBHOOK_SECRET",
24
+ },
25
+ },
26
+ },
27
+ });
28
+ expect(result.success).toBe(true);
29
+ });
30
+ });
@@ -1,5 +1,6 @@
1
1
  import { MarkdownConfigSchema } from "openclaw/plugin-sdk";
2
2
  import { z } from "zod";
3
+ import { buildSecretInputSchema } from "./secret-input.js";
3
4
 
4
5
  const allowFromEntry = z.union([z.string(), z.number()]);
5
6
 
@@ -7,10 +8,10 @@ const zaloAccountSchema = z.object({
7
8
  name: z.string().optional(),
8
9
  enabled: z.boolean().optional(),
9
10
  markdown: MarkdownConfigSchema,
10
- botToken: z.string().optional(),
11
+ botToken: buildSecretInputSchema().optional(),
11
12
  tokenFile: z.string().optional(),
12
13
  webhookUrl: z.string().optional(),
13
- webhookSecret: z.string().optional(),
14
+ webhookSecret: buildSecretInputSchema().optional(),
14
15
  webhookPath: z.string().optional(),
15
16
  dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
16
17
  allowFrom: z.array(allowFromEntry).optional(),
package/src/monitor.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk";
3
3
  import {
4
+ createScopedPairingAccess,
4
5
  createReplyPrefixOptions,
5
- resolveSenderCommandAuthorization,
6
+ resolveDirectDmAuthorizationOutcome,
7
+ resolveSenderCommandAuthorizationWithRuntime,
6
8
  resolveOutboundMediaUrls,
7
9
  resolveDefaultGroupPolicy,
10
+ resolveInboundRouteEnvelopeBuilderWithRuntime,
8
11
  sendMediaWithLeadingCaption,
9
12
  resolveWebhookPath,
10
13
  warnMissingProviderGroupPolicyFallbackOnce,
@@ -27,6 +30,9 @@ import {
27
30
  resolveZaloRuntimeGroupPolicy,
28
31
  } from "./group-access.js";
29
32
  import {
33
+ clearZaloWebhookSecurityStateForTest,
34
+ getZaloWebhookRateLimitStateSizeForTest,
35
+ getZaloWebhookStatusCounterSizeForTest,
30
36
  handleZaloWebhookRequest as handleZaloWebhookRequestInternal,
31
37
  registerZaloWebhookTarget as registerZaloWebhookTargetInternal,
32
38
  type ZaloWebhookTarget,
@@ -69,9 +75,32 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str
69
75
  }
70
76
 
71
77
  export function registerZaloWebhookTarget(target: ZaloWebhookTarget): () => void {
72
- return registerZaloWebhookTargetInternal(target);
78
+ return registerZaloWebhookTargetInternal(target, {
79
+ route: {
80
+ auth: "plugin",
81
+ match: "exact",
82
+ pluginId: "zalo",
83
+ source: "zalo-webhook",
84
+ accountId: target.account.accountId,
85
+ log: target.runtime.log,
86
+ handler: async (req, res) => {
87
+ const handled = await handleZaloWebhookRequest(req, res);
88
+ if (!handled && !res.headersSent) {
89
+ res.statusCode = 404;
90
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
91
+ res.end("Not Found");
92
+ }
93
+ },
94
+ },
95
+ });
73
96
  }
74
97
 
98
+ export {
99
+ clearZaloWebhookSecurityStateForTest,
100
+ getZaloWebhookRateLimitStateSizeForTest,
101
+ getZaloWebhookStatusCounterSizeForTest,
102
+ };
103
+
75
104
  export async function handleZaloWebhookRequest(
76
105
  req: IncomingMessage,
77
106
  res: ServerResponse,
@@ -142,7 +171,7 @@ function startPollingLoop(params: {
142
171
  if (err instanceof ZaloApiError && err.isPollingTimeout) {
143
172
  // no updates
144
173
  } else if (!isStopped() && !abortSignal.aborted) {
145
- console.error(`[${account.accountId}] Zalo polling error:`, err);
174
+ runtime.error?.(`[${account.accountId}] Zalo polling error: ${String(err)}`);
146
175
  await new Promise((resolve) => setTimeout(resolve, 5000));
147
176
  }
148
177
  }
@@ -189,10 +218,12 @@ async function processUpdate(
189
218
  );
190
219
  break;
191
220
  case "message.sticker.received":
192
- console.log(`[${account.accountId}] Received sticker from ${message.from.id}`);
221
+ logVerbose(core, runtime, `[${account.accountId}] Received sticker from ${message.from.id}`);
193
222
  break;
194
223
  case "message.unsupported.received":
195
- console.log(
224
+ logVerbose(
225
+ core,
226
+ runtime,
196
227
  `[${account.accountId}] Received unsupported message type from ${message.from.id}`,
197
228
  );
198
229
  break;
@@ -258,7 +289,7 @@ async function handleImageMessage(
258
289
  mediaPath = saved.path;
259
290
  mediaType = saved.contentType;
260
291
  } catch (err) {
261
- console.error(`[${account.accountId}] Failed to download Zalo image:`, err);
292
+ runtime.error?.(`[${account.accountId}] Failed to download Zalo image: ${String(err)}`);
262
293
  }
263
294
  }
264
295
 
@@ -303,6 +334,11 @@ async function processMessageWithPipeline(params: {
303
334
  statusSink,
304
335
  fetcher,
305
336
  } = params;
337
+ const pairing = createScopedPairingAccess({
338
+ core,
339
+ channel: "zalo",
340
+ accountId: account.accountId,
341
+ });
306
342
  const { from, chat, message_id, date } = message;
307
343
 
308
344
  const isGroup = chat.chat_type === "GROUP";
@@ -349,82 +385,76 @@ async function processMessageWithPipeline(params: {
349
385
  }
350
386
 
351
387
  const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
352
- const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorization({
353
- cfg: config,
354
- rawBody,
388
+ const { senderAllowedForCommands, commandAuthorized } =
389
+ await resolveSenderCommandAuthorizationWithRuntime({
390
+ cfg: config,
391
+ rawBody,
392
+ isGroup,
393
+ dmPolicy,
394
+ configuredAllowFrom: configAllowFrom,
395
+ configuredGroupAllowFrom: groupAllowFrom,
396
+ senderId,
397
+ isSenderAllowed: isZaloSenderAllowed,
398
+ readAllowFromStore: pairing.readAllowFromStore,
399
+ runtime: core.channel.commands,
400
+ });
401
+
402
+ const directDmOutcome = resolveDirectDmAuthorizationOutcome({
355
403
  isGroup,
356
404
  dmPolicy,
357
- configuredAllowFrom: configAllowFrom,
358
- senderId,
359
- isSenderAllowed: isZaloSenderAllowed,
360
- readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
361
- shouldComputeCommandAuthorized: (body, cfg) =>
362
- core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
363
- resolveCommandAuthorizedFromAuthorizers: (params) =>
364
- core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
405
+ senderAllowedForCommands,
365
406
  });
366
-
367
- if (!isGroup) {
368
- if (dmPolicy === "disabled") {
369
- logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
370
- return;
371
- }
372
-
373
- if (dmPolicy !== "open") {
374
- const allowed = senderAllowedForCommands;
375
-
376
- if (!allowed) {
377
- if (dmPolicy === "pairing") {
378
- const { code, created } = await core.channel.pairing.upsertPairingRequest({
379
- channel: "zalo",
380
- id: senderId,
381
- meta: { name: senderName ?? undefined },
382
- });
383
-
384
- if (created) {
385
- logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
386
- try {
387
- await sendMessage(
388
- token,
389
- {
390
- chat_id: chatId,
391
- text: core.channel.pairing.buildPairingReply({
392
- channel: "zalo",
393
- idLine: `Your Zalo user id: ${senderId}`,
394
- code,
395
- }),
396
- },
397
- fetcher,
398
- );
399
- statusSink?.({ lastOutboundAt: Date.now() });
400
- } catch (err) {
401
- logVerbose(
402
- core,
403
- runtime,
404
- `zalo pairing reply failed for ${senderId}: ${String(err)}`,
405
- );
406
- }
407
- }
408
- } else {
409
- logVerbose(
410
- core,
411
- runtime,
412
- `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
407
+ if (directDmOutcome === "disabled") {
408
+ logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
409
+ return;
410
+ }
411
+ if (directDmOutcome === "unauthorized") {
412
+ if (dmPolicy === "pairing") {
413
+ const { code, created } = await pairing.upsertPairingRequest({
414
+ id: senderId,
415
+ meta: { name: senderName ?? undefined },
416
+ });
417
+
418
+ if (created) {
419
+ logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
420
+ try {
421
+ await sendMessage(
422
+ token,
423
+ {
424
+ chat_id: chatId,
425
+ text: core.channel.pairing.buildPairingReply({
426
+ channel: "zalo",
427
+ idLine: `Your Zalo user id: ${senderId}`,
428
+ code,
429
+ }),
430
+ },
431
+ fetcher,
413
432
  );
433
+ statusSink?.({ lastOutboundAt: Date.now() });
434
+ } catch (err) {
435
+ logVerbose(core, runtime, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
414
436
  }
415
- return;
416
437
  }
438
+ } else {
439
+ logVerbose(
440
+ core,
441
+ runtime,
442
+ `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
443
+ );
417
444
  }
445
+ return;
418
446
  }
419
447
 
420
- const route = core.channel.routing.resolveAgentRoute({
448
+ const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
421
449
  cfg: config,
422
450
  channel: "zalo",
423
451
  accountId: account.accountId,
424
452
  peer: {
425
- kind: isGroup ? "group" : "direct",
453
+ kind: isGroup ? ("group" as const) : ("direct" as const),
426
454
  id: chatId,
427
455
  },
456
+ runtime: core.channel,
457
+ sessionStore: config.session?.store,
428
458
  });
429
459
 
430
460
  if (
@@ -437,20 +467,10 @@ async function processMessageWithPipeline(params: {
437
467
  }
438
468
 
439
469
  const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
440
- const storePath = core.channel.session.resolveStorePath(config.session?.store, {
441
- agentId: route.agentId,
442
- });
443
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
444
- const previousTimestamp = core.channel.session.readSessionUpdatedAt({
445
- storePath,
446
- sessionKey: route.sessionKey,
447
- });
448
- const body = core.channel.reply.formatAgentEnvelope({
470
+ const { storePath, body } = buildEnvelope({
449
471
  channel: "Zalo",
450
472
  from: fromLabel,
451
473
  timestamp: date ? date * 1000 : undefined,
452
- previousTimestamp,
453
- envelope: envelopeOptions,
454
474
  body: rawBody,
455
475
  });
456
476