@openclaw/zalouser 2026.3.10 → 2026.3.12

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.12
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.3.11
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
3
15
  ## 2026.3.10
4
16
 
5
17
  ### Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/zalouser",
3
- "version": "2026.3.10",
3
+ "version": "2026.3.12",
4
4
  "description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -5,6 +5,7 @@ import {
5
5
  primeSendMock,
6
6
  } from "../../../src/test-utils/send-payload-contract.js";
7
7
  import { zalouserPlugin } from "./channel.js";
8
+ import { setZalouserRuntime } from "./runtime.js";
8
9
 
9
10
  vi.mock("./send.js", () => ({
10
11
  sendMessageZalouser: vi.fn().mockResolvedValue({ ok: true, messageId: "zlu-1" }),
@@ -38,6 +39,14 @@ describe("zalouserPlugin outbound sendPayload", () => {
38
39
  let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalouser"]>>;
39
40
 
40
41
  beforeEach(async () => {
42
+ setZalouserRuntime({
43
+ channel: {
44
+ text: {
45
+ resolveChunkMode: vi.fn(() => "length"),
46
+ resolveTextChunkLimit: vi.fn(() => 1200),
47
+ },
48
+ },
49
+ } as never);
41
50
  const mod = await import("./send.js");
42
51
  mockedSend = vi.mocked(mod.sendMessageZalouser);
43
52
  mockedSend.mockClear();
@@ -55,7 +64,7 @@ describe("zalouserPlugin outbound sendPayload", () => {
55
64
  expect(mockedSend).toHaveBeenCalledWith(
56
65
  "1471383327500481391",
57
66
  "hello group",
58
- expect.objectContaining({ isGroup: true }),
67
+ expect.objectContaining({ isGroup: true, textMode: "markdown" }),
59
68
  );
60
69
  expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g1" });
61
70
  });
@@ -71,7 +80,7 @@ describe("zalouserPlugin outbound sendPayload", () => {
71
80
  expect(mockedSend).toHaveBeenCalledWith(
72
81
  "987654321",
73
82
  "hello",
74
- expect.objectContaining({ isGroup: false }),
83
+ expect.objectContaining({ isGroup: false, textMode: "markdown" }),
75
84
  );
76
85
  expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-d1" });
77
86
  });
@@ -87,14 +96,37 @@ describe("zalouserPlugin outbound sendPayload", () => {
87
96
  expect(mockedSend).toHaveBeenCalledWith(
88
97
  "g-1471383327500481391",
89
98
  "hello native group",
90
- expect.objectContaining({ isGroup: true }),
99
+ expect.objectContaining({ isGroup: true, textMode: "markdown" }),
91
100
  );
92
101
  expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g-native" });
93
102
  });
94
103
 
104
+ it("passes long markdown through once so formatting happens before chunking", async () => {
105
+ const text = `**${"a".repeat(2501)}**`;
106
+ mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-code" });
107
+
108
+ const result = await zalouserPlugin.outbound!.sendPayload!({
109
+ ...baseCtx({ text }),
110
+ to: "987654321",
111
+ });
112
+
113
+ expect(mockedSend).toHaveBeenCalledTimes(1);
114
+ expect(mockedSend).toHaveBeenCalledWith(
115
+ "987654321",
116
+ text,
117
+ expect.objectContaining({
118
+ isGroup: false,
119
+ textMode: "markdown",
120
+ textChunkMode: "length",
121
+ textChunkLimit: 1200,
122
+ }),
123
+ );
124
+ expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-code" });
125
+ });
126
+
95
127
  installSendPayloadContractSuite({
96
128
  channel: "zalouser",
97
- chunking: { mode: "split", longTextLength: 3000, maxChunkLength: 2000 },
129
+ chunking: { mode: "passthrough", longTextLength: 3000 },
98
130
  createHarness: ({ payload, sendResults }) => {
99
131
  primeSendMock(mockedSend, { ok: true, messageId: "zlu-1" }, sendResults);
100
132
  return {
@@ -1,30 +1,65 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { zalouserPlugin } from "./channel.js";
3
- import { sendReactionZalouser } from "./send.js";
3
+ import { setZalouserRuntime } from "./runtime.js";
4
+ import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
4
5
 
5
6
  vi.mock("./send.js", async (importOriginal) => {
6
7
  const actual = (await importOriginal()) as Record<string, unknown>;
7
8
  return {
8
9
  ...actual,
10
+ sendMessageZalouser: vi.fn(async () => ({ ok: true, messageId: "mid-1" })),
9
11
  sendReactionZalouser: vi.fn(async () => ({ ok: true })),
10
12
  };
11
13
  });
12
14
 
15
+ const mockSendMessage = vi.mocked(sendMessageZalouser);
13
16
  const mockSendReaction = vi.mocked(sendReactionZalouser);
14
17
 
15
- describe("zalouser outbound chunker", () => {
16
- it("chunks without empty strings and respects limit", () => {
17
- const chunker = zalouserPlugin.outbound?.chunker;
18
- expect(chunker).toBeTypeOf("function");
19
- if (!chunker) {
18
+ describe("zalouser outbound", () => {
19
+ beforeEach(() => {
20
+ mockSendMessage.mockClear();
21
+ setZalouserRuntime({
22
+ channel: {
23
+ text: {
24
+ resolveChunkMode: vi.fn(() => "newline"),
25
+ resolveTextChunkLimit: vi.fn(() => 10),
26
+ },
27
+ },
28
+ } as never);
29
+ });
30
+
31
+ it("passes markdown chunk settings through sendText", async () => {
32
+ const sendText = zalouserPlugin.outbound?.sendText;
33
+ expect(sendText).toBeTypeOf("function");
34
+ if (!sendText) {
20
35
  return;
21
36
  }
22
37
 
23
- const limit = 10;
24
- const chunks = chunker("hello world\nthis is a test", limit);
25
- expect(chunks.length).toBeGreaterThan(1);
26
- expect(chunks.every((c) => c.length > 0)).toBe(true);
27
- expect(chunks.every((c) => c.length <= limit)).toBe(true);
38
+ const result = await sendText({
39
+ cfg: { channels: { zalouser: { enabled: true } } } as never,
40
+ to: "group:123456",
41
+ text: "hello world\nthis is a test",
42
+ accountId: "default",
43
+ } as never);
44
+
45
+ expect(mockSendMessage).toHaveBeenCalledWith(
46
+ "123456",
47
+ "hello world\nthis is a test",
48
+ expect.objectContaining({
49
+ profile: "default",
50
+ isGroup: true,
51
+ textMode: "markdown",
52
+ textChunkMode: "newline",
53
+ textChunkLimit: 10,
54
+ }),
55
+ );
56
+ expect(result).toEqual(
57
+ expect.objectContaining({
58
+ channel: "zalouser",
59
+ messageId: "mid-1",
60
+ ok: true,
61
+ }),
62
+ );
28
63
  });
29
64
  });
30
65
 
package/src/channel.ts CHANGED
@@ -20,9 +20,9 @@ import {
20
20
  buildBaseAccountStatusSnapshot,
21
21
  buildChannelConfigSchema,
22
22
  DEFAULT_ACCOUNT_ID,
23
- chunkTextForOutbound,
24
23
  deleteAccountFromConfigSection,
25
24
  formatAllowFromLowercase,
25
+ isDangerousNameMatchingEnabled,
26
26
  isNumericTargetId,
27
27
  migrateBaseNameToDefaultAccount,
28
28
  normalizeAccountId,
@@ -43,6 +43,7 @@ import { resolveZalouserReactionMessageIds } from "./message-sid.js";
43
43
  import { zalouserOnboardingAdapter } from "./onboarding.js";
44
44
  import { probeZalouser } from "./probe.js";
45
45
  import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
46
+ import { getZalouserRuntime } from "./runtime.js";
46
47
  import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
47
48
  import { collectZalouserStatusIssues } from "./status-issues.js";
48
49
  import {
@@ -166,6 +167,16 @@ function resolveZalouserQrProfile(accountId?: string | null): string {
166
167
  return normalized;
167
168
  }
168
169
 
170
+ function resolveZalouserOutboundChunkMode(cfg: OpenClawConfig, accountId?: string) {
171
+ return getZalouserRuntime().channel.text.resolveChunkMode(cfg, "zalouser", accountId);
172
+ }
173
+
174
+ function resolveZalouserOutboundTextChunkLimit(cfg: OpenClawConfig, accountId?: string) {
175
+ return getZalouserRuntime().channel.text.resolveTextChunkLimit(cfg, "zalouser", accountId, {
176
+ fallbackLimit: zalouserDock.outbound?.textChunkLimit ?? 2000,
177
+ });
178
+ }
179
+
169
180
  function mapUser(params: {
170
181
  id: string;
171
182
  name?: string | null;
@@ -206,6 +217,7 @@ function resolveZalouserGroupPolicyEntry(params: ChannelGroupContext) {
206
217
  groupId: params.groupId,
207
218
  groupChannel: params.groupChannel,
208
219
  includeWildcard: true,
220
+ allowNameMatching: isDangerousNameMatchingEnabled(account.config),
209
221
  }),
210
222
  );
211
223
  }
@@ -595,14 +607,11 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
595
607
  },
596
608
  outbound: {
597
609
  deliveryMode: "direct",
598
- chunker: chunkTextForOutbound,
599
- chunkerMode: "text",
600
- textChunkLimit: 2000,
610
+ chunker: (text, limit) => getZalouserRuntime().channel.text.chunkMarkdownText(text, limit),
611
+ chunkerMode: "markdown",
601
612
  sendPayload: async (ctx) =>
602
613
  await sendPayloadWithChunkedTextAndMedia({
603
614
  ctx,
604
- textChunkLimit: zalouserPlugin.outbound!.textChunkLimit,
605
- chunker: zalouserPlugin.outbound!.chunker,
606
615
  sendText: (nextCtx) => zalouserPlugin.outbound!.sendText!(nextCtx),
607
616
  sendMedia: (nextCtx) => zalouserPlugin.outbound!.sendMedia!(nextCtx),
608
617
  emptyResult: { channel: "zalouser", messageId: "" },
@@ -613,6 +622,9 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
613
622
  const result = await sendMessageZalouser(target.threadId, text, {
614
623
  profile: account.profile,
615
624
  isGroup: target.isGroup,
625
+ textMode: "markdown",
626
+ textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
627
+ textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId),
616
628
  });
617
629
  return buildChannelSendResult("zalouser", result);
618
630
  },
@@ -624,6 +636,9 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
624
636
  isGroup: target.isGroup,
625
637
  mediaUrl,
626
638
  mediaLocalRoots,
639
+ textMode: "markdown",
640
+ textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
641
+ textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId),
627
642
  });
628
643
  return buildChannelSendResult("zalouser", result);
629
644
  },
@@ -19,6 +19,7 @@ const zalouserAccountSchema = z.object({
19
19
  enabled: z.boolean().optional(),
20
20
  markdown: MarkdownConfigSchema,
21
21
  profile: z.string().optional(),
22
+ dangerouslyAllowNameMatching: z.boolean().optional(),
22
23
  dmPolicy: DmPolicySchema.optional(),
23
24
  allowFrom: AllowFromListSchema,
24
25
  historyLimit: z.number().int().min(0).optional(),
@@ -23,6 +23,18 @@ describe("zalouser group policy helpers", () => {
23
23
  ).toEqual(["123", "group:123", "chan-1", "Team Alpha", "team-alpha", "*"]);
24
24
  });
25
25
 
26
+ it("builds id-only candidates when name matching is disabled", () => {
27
+ expect(
28
+ buildZalouserGroupCandidates({
29
+ groupId: "123",
30
+ groupChannel: "chan-1",
31
+ groupName: "Team Alpha",
32
+ includeGroupIdAlias: true,
33
+ allowNameMatching: false,
34
+ }),
35
+ ).toEqual(["123", "group:123", "*"]);
36
+ });
37
+
26
38
  it("finds the first matching group entry", () => {
27
39
  const groups = {
28
40
  "group:123": { allow: true },
@@ -23,6 +23,7 @@ export function buildZalouserGroupCandidates(params: {
23
23
  groupName?: string | null;
24
24
  includeGroupIdAlias?: boolean;
25
25
  includeWildcard?: boolean;
26
+ allowNameMatching?: boolean;
26
27
  }): string[] {
27
28
  const seen = new Set<string>();
28
29
  const out: string[] = [];
@@ -43,10 +44,12 @@ export function buildZalouserGroupCandidates(params: {
43
44
  if (params.includeGroupIdAlias === true && groupId) {
44
45
  push(`group:${groupId}`);
45
46
  }
46
- push(groupChannel);
47
- push(groupName);
48
- if (groupName) {
49
- push(normalizeZalouserGroupSlug(groupName));
47
+ if (params.allowNameMatching !== false) {
48
+ push(groupChannel);
49
+ push(groupName);
50
+ if (groupName) {
51
+ push(normalizeZalouserGroupSlug(groupName));
52
+ }
50
53
  }
51
54
  if (params.includeWildcard !== false) {
52
55
  push("*");
@@ -51,6 +51,7 @@ function createRuntimeEnv(): RuntimeEnv {
51
51
 
52
52
  function installRuntime(params: {
53
53
  commandAuthorized?: boolean;
54
+ replyPayload?: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
54
55
  resolveCommandAuthorizedFromAuthorizers?: (params: {
55
56
  useAccessGroups: boolean;
56
57
  authorizers: Array<{ configured: boolean; allowed: boolean }>;
@@ -58,6 +59,9 @@ function installRuntime(params: {
58
59
  }) {
59
60
  const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions, ctx }) => {
60
61
  await dispatcherOptions.typingCallbacks?.onReplyStart?.();
62
+ if (params.replyPayload) {
63
+ await dispatcherOptions.deliver(params.replyPayload);
64
+ }
61
65
  return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 }, ctx };
62
66
  });
63
67
  const resolveCommandAuthorizedFromAuthorizers = vi.fn(
@@ -166,7 +170,8 @@ function installRuntime(params: {
166
170
  text: {
167
171
  resolveMarkdownTableMode: vi.fn(() => "code"),
168
172
  convertMarkdownTables: vi.fn((text: string) => text),
169
- resolveChunkMode: vi.fn(() => "line"),
173
+ resolveChunkMode: vi.fn(() => "length"),
174
+ resolveTextChunkLimit: vi.fn(() => 1200),
170
175
  chunkMarkdownTextWithMode: vi.fn((text: string) => [text]),
171
176
  },
172
177
  },
@@ -304,6 +309,42 @@ describe("zalouser monitor group mention gating", () => {
304
309
  expect(callArg?.ctx?.WasMentioned).toBe(true);
305
310
  });
306
311
 
312
+ it("passes long markdown replies through once so formatting happens before chunking", async () => {
313
+ const replyText = `**${"a".repeat(2501)}**`;
314
+ installRuntime({
315
+ commandAuthorized: false,
316
+ replyPayload: { text: replyText },
317
+ });
318
+
319
+ await __testing.processMessage({
320
+ message: createDmMessage({
321
+ content: "hello",
322
+ }),
323
+ account: {
324
+ ...createAccount(),
325
+ config: {
326
+ ...createAccount().config,
327
+ dmPolicy: "open",
328
+ },
329
+ },
330
+ config: createConfig(),
331
+ runtime: createRuntimeEnv(),
332
+ });
333
+
334
+ expect(sendMessageZalouserMock).toHaveBeenCalledTimes(1);
335
+ expect(sendMessageZalouserMock).toHaveBeenCalledWith(
336
+ "u-1",
337
+ replyText,
338
+ expect.objectContaining({
339
+ isGroup: false,
340
+ profile: "default",
341
+ textMode: "markdown",
342
+ textChunkMode: "length",
343
+ textChunkLimit: 1200,
344
+ }),
345
+ );
346
+ });
347
+
307
348
  it("uses commandContent for mention-prefixed control commands", async () => {
308
349
  const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
309
350
  commandAuthorized: true,
@@ -383,6 +424,73 @@ describe("zalouser monitor group mention gating", () => {
383
424
  expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
384
425
  });
385
426
 
427
+ it("does not accept a different group id by matching only the mutable group name by default", async () => {
428
+ const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
429
+ commandAuthorized: false,
430
+ });
431
+ await __testing.processMessage({
432
+ message: createGroupMessage({
433
+ threadId: "g-attacker-001",
434
+ groupName: "Trusted Team",
435
+ senderId: "666",
436
+ hasAnyMention: true,
437
+ wasExplicitlyMentioned: true,
438
+ content: "ping @bot",
439
+ }),
440
+ account: {
441
+ ...createAccount(),
442
+ config: {
443
+ ...createAccount().config,
444
+ groupPolicy: "allowlist",
445
+ groupAllowFrom: ["*"],
446
+ groups: {
447
+ "group:g-trusted-001": { allow: true },
448
+ "Trusted Team": { allow: true },
449
+ },
450
+ },
451
+ },
452
+ config: createConfig(),
453
+ runtime: createRuntimeEnv(),
454
+ });
455
+
456
+ expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
457
+ });
458
+
459
+ it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
460
+ const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
461
+ commandAuthorized: false,
462
+ });
463
+ await __testing.processMessage({
464
+ message: createGroupMessage({
465
+ threadId: "g-attacker-001",
466
+ groupName: "Trusted Team",
467
+ senderId: "666",
468
+ hasAnyMention: true,
469
+ wasExplicitlyMentioned: true,
470
+ content: "ping @bot",
471
+ }),
472
+ account: {
473
+ ...createAccount(),
474
+ config: {
475
+ ...createAccount().config,
476
+ dangerouslyAllowNameMatching: true,
477
+ groupPolicy: "allowlist",
478
+ groupAllowFrom: ["*"],
479
+ groups: {
480
+ "group:g-trusted-001": { allow: true },
481
+ "Trusted Team": { allow: true },
482
+ },
483
+ },
484
+ },
485
+ config: createConfig(),
486
+ runtime: createRuntimeEnv(),
487
+ });
488
+
489
+ expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
490
+ const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
491
+ expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
492
+ });
493
+
386
494
  it("allows group control commands when sender is in groupAllowFrom", async () => {
387
495
  const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
388
496
  installRuntime({
package/src/monitor.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  createScopedPairingAccess,
20
20
  createReplyPrefixOptions,
21
21
  evaluateGroupRouteAccessForPolicy,
22
+ isDangerousNameMatchingEnabled,
22
23
  issuePairingChallenge,
23
24
  resolveOutboundMediaUrls,
24
25
  mergeAllowlist,
@@ -212,6 +213,7 @@ function resolveGroupRequireMention(params: {
212
213
  groupId: string;
213
214
  groupName?: string | null;
214
215
  groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
216
+ allowNameMatching?: boolean;
215
217
  }): boolean {
216
218
  const entry = findZalouserGroupEntry(
217
219
  params.groups ?? {},
@@ -220,6 +222,7 @@ function resolveGroupRequireMention(params: {
220
222
  groupName: params.groupName,
221
223
  includeGroupIdAlias: true,
222
224
  includeWildcard: true,
225
+ allowNameMatching: params.allowNameMatching,
223
226
  }),
224
227
  );
225
228
  if (typeof entry?.requireMention === "boolean") {
@@ -316,6 +319,7 @@ async function processMessage(
316
319
  });
317
320
 
318
321
  const groups = account.config.groups ?? {};
322
+ const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
319
323
  if (isGroup) {
320
324
  const groupEntry = findZalouserGroupEntry(
321
325
  groups,
@@ -324,6 +328,7 @@ async function processMessage(
324
328
  groupName,
325
329
  includeGroupIdAlias: true,
326
330
  includeWildcard: true,
331
+ allowNameMatching,
327
332
  }),
328
333
  );
329
334
  const routeAccess = evaluateGroupRouteAccessForPolicy({
@@ -466,6 +471,7 @@ async function processMessage(
466
471
  groupId: chatId,
467
472
  groupName,
468
473
  groups,
474
+ allowNameMatching,
469
475
  })
470
476
  : false;
471
477
  const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
@@ -703,6 +709,10 @@ async function deliverZalouserReply(params: {
703
709
  params;
704
710
  const tableMode = params.tableMode ?? "code";
705
711
  const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
712
+ const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
713
+ const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, {
714
+ fallbackLimit: ZALOUSER_TEXT_LIMIT,
715
+ });
706
716
 
707
717
  const sentMedia = await sendMediaWithLeadingCaption({
708
718
  mediaUrls: resolveOutboundMediaUrls(payload),
@@ -713,6 +723,9 @@ async function deliverZalouserReply(params: {
713
723
  profile,
714
724
  mediaUrl,
715
725
  isGroup,
726
+ textMode: "markdown",
727
+ textChunkMode: chunkMode,
728
+ textChunkLimit,
716
729
  });
717
730
  statusSink?.({ lastOutboundAt: Date.now() });
718
731
  },
@@ -725,20 +738,17 @@ async function deliverZalouserReply(params: {
725
738
  }
726
739
 
727
740
  if (text) {
728
- const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
729
- const chunks = core.channel.text.chunkMarkdownTextWithMode(
730
- text,
731
- ZALOUSER_TEXT_LIMIT,
732
- chunkMode,
733
- );
734
- logVerbose(core, runtime, `Sending ${chunks.length} text chunk(s) to ${chatId}`);
735
- for (const chunk of chunks) {
736
- try {
737
- await sendMessageZalouser(chatId, chunk, { profile, isGroup });
738
- statusSink?.({ lastOutboundAt: Date.now() });
739
- } catch (err) {
740
- runtime.error(`Zalouser message send failed: ${String(err)}`);
741
- }
741
+ try {
742
+ await sendMessageZalouser(chatId, text, {
743
+ profile,
744
+ isGroup,
745
+ textMode: "markdown",
746
+ textChunkMode: chunkMode,
747
+ textChunkLimit,
748
+ });
749
+ statusSink?.({ lastOutboundAt: Date.now() });
750
+ } catch (err) {
751
+ runtime.error(`Zalouser message send failed: ${String(err)}`);
742
752
  }
743
753
  }
744
754
  }