@openclaw/zalouser 2026.3.13 → 2026.5.1-beta.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.
Files changed (67) hide show
  1. package/README.md +4 -3
  2. package/api.ts +9 -0
  3. package/channel-plugin-api.ts +3 -0
  4. package/contract-api.ts +2 -0
  5. package/doctor-contract-api.ts +1 -0
  6. package/index.ts +29 -24
  7. package/openclaw.plugin.json +288 -1
  8. package/package.json +38 -11
  9. package/runtime-api.ts +67 -0
  10. package/secret-contract-api.ts +4 -0
  11. package/setup-entry.ts +9 -0
  12. package/setup-plugin-api.ts +2 -0
  13. package/src/accounts.runtime.ts +1 -0
  14. package/src/accounts.test-mocks.ts +7 -3
  15. package/src/accounts.test.ts +53 -1
  16. package/src/accounts.ts +38 -24
  17. package/src/channel-api.ts +20 -0
  18. package/src/channel.adapters.ts +390 -0
  19. package/src/channel.directory.test.ts +47 -40
  20. package/src/channel.runtime.ts +12 -0
  21. package/src/channel.sendpayload.test.ts +41 -23
  22. package/src/channel.setup.test.ts +33 -0
  23. package/src/channel.setup.ts +12 -0
  24. package/src/channel.test.ts +231 -20
  25. package/src/channel.ts +176 -685
  26. package/src/config-schema.ts +5 -5
  27. package/src/directory.ts +54 -0
  28. package/src/doctor-contract.ts +156 -0
  29. package/src/doctor.test.ts +77 -0
  30. package/src/doctor.ts +37 -0
  31. package/src/group-policy.test.ts +4 -4
  32. package/src/group-policy.ts +4 -2
  33. package/src/monitor.account-scope.test.ts +2 -1
  34. package/src/monitor.group-gating.test.ts +162 -8
  35. package/src/monitor.ts +233 -173
  36. package/src/probe.ts +3 -2
  37. package/src/qr-temp-file.ts +1 -1
  38. package/src/reaction.ts +5 -2
  39. package/src/runtime.ts +6 -3
  40. package/src/security-audit.test.ts +80 -0
  41. package/src/security-audit.ts +71 -0
  42. package/src/send.test.ts +2 -2
  43. package/src/send.ts +3 -3
  44. package/src/session-route.ts +121 -0
  45. package/src/setup-core.ts +33 -0
  46. package/src/setup-surface.test.ts +363 -0
  47. package/src/setup-surface.ts +470 -0
  48. package/src/setup-test-helpers.ts +42 -0
  49. package/src/shared.ts +92 -0
  50. package/src/status-issues.test.ts +1 -13
  51. package/src/status-issues.ts +8 -2
  52. package/src/test-helpers.ts +1 -1
  53. package/src/text-styles.test.ts +1 -1
  54. package/src/text-styles.ts +5 -2
  55. package/src/tool.test.ts +66 -3
  56. package/src/tool.ts +76 -14
  57. package/src/types.ts +3 -3
  58. package/src/zalo-js.credentials.test.ts +465 -0
  59. package/src/zalo-js.test-mocks.ts +89 -0
  60. package/src/zalo-js.ts +491 -274
  61. package/src/zca-client.test.ts +24 -0
  62. package/src/zca-client.ts +24 -58
  63. package/src/zca-constants.ts +55 -0
  64. package/test-api.ts +21 -0
  65. package/tsconfig.json +16 -0
  66. package/CHANGELOG.md +0 -107
  67. package/src/onboarding.ts +0 -340
package/src/monitor.ts CHANGED
@@ -1,37 +1,45 @@
1
+ import { mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
2
+ import {
3
+ implicitMentionKindWhen,
4
+ resolveInboundMentionDecision,
5
+ } from "openclaw/plugin-sdk/channel-inbound";
6
+ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
1
7
  import {
2
8
  DM_GROUP_ACCESS_REASON,
9
+ resolveDmGroupAccessWithLists,
10
+ } from "openclaw/plugin-sdk/channel-policy";
11
+ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
12
+ import { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
13
+ import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
14
+ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core";
15
+ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
16
+ import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
17
+ import {
18
+ evaluateGroupRouteAccessForPolicy,
19
+ resolveSenderScopedGroupPolicy,
20
+ } from "openclaw/plugin-sdk/group-access";
21
+ import {
3
22
  DEFAULT_GROUP_HISTORY_LIMIT,
4
23
  type HistoryEntry,
5
- KeyedAsyncQueue,
6
24
  buildPendingHistoryContextFromMap,
7
25
  clearHistoryEntriesIfEnabled,
8
26
  recordPendingHistoryEntryIfEnabled,
9
- resolveDmGroupAccessWithLists,
10
- } from "openclaw/plugin-sdk/compat";
11
- import type {
12
- MarkdownTableMode,
13
- OpenClawConfig,
14
- OutboundReplyPayload,
15
- RuntimeEnv,
16
- } from "openclaw/plugin-sdk/zalouser";
27
+ } from "openclaw/plugin-sdk/reply-history";
28
+ import {
29
+ deliverTextOrMediaReply,
30
+ resolveSendableOutboundReplyParts,
31
+ type OutboundReplyPayload,
32
+ } from "openclaw/plugin-sdk/reply-payload";
33
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
17
34
  import {
18
- createTypingCallbacks,
19
- createScopedPairingAccess,
20
- createReplyPrefixOptions,
21
- evaluateGroupRouteAccessForPolicy,
22
- isDangerousNameMatchingEnabled,
23
- issuePairingChallenge,
24
- resolveOutboundMediaUrls,
25
- mergeAllowlist,
26
- resolveMentionGatingWithBypass,
27
- resolveOpenProviderRuntimeGroupPolicy,
28
35
  resolveDefaultGroupPolicy,
29
- resolveSenderCommandAuthorization,
30
- sendMediaWithLeadingCaption,
31
- summarizeMapping,
36
+ resolveOpenProviderRuntimeGroupPolicy,
32
37
  warnMissingProviderGroupPolicyFallbackOnce,
33
- } from "openclaw/plugin-sdk/zalouser";
34
- import { createDeferred } from "../../shared/deferred.js";
38
+ } from "openclaw/plugin-sdk/runtime-group-policy";
39
+ import {
40
+ normalizeLowercaseStringOrEmpty,
41
+ normalizeOptionalLowercaseString,
42
+ } from "openclaw/plugin-sdk/text-runtime";
35
43
  import {
36
44
  buildZalouserGroupCandidates,
37
45
  findZalouserGroupEntry,
@@ -74,7 +82,7 @@ function normalizeZalouserEntry(entry: string): string {
74
82
  function buildNameIndex<T>(items: T[], nameFn: (item: T) => string | undefined): Map<string, T[]> {
75
83
  const index = new Map<string, T[]>();
76
84
  for (const item of items) {
77
- const name = nameFn(item)?.trim().toLowerCase();
85
+ const name = normalizeOptionalLowercaseString(nameFn(item));
78
86
  if (!name) {
79
87
  continue;
80
88
  }
@@ -101,9 +109,9 @@ function resolveUserAllowlistEntries(
101
109
  additions.push(entry);
102
110
  continue;
103
111
  }
104
- const matches = byName.get(entry.toLowerCase()) ?? [];
112
+ const matches = byName.get(normalizeLowercaseStringOrEmpty(entry)) ?? [];
105
113
  const match = matches[0];
106
- const id = match?.userId ? String(match.userId) : undefined;
114
+ const id = match?.userId;
107
115
  if (id) {
108
116
  additions.push(id);
109
117
  mapping.push(`${entry}->${id}`);
@@ -147,24 +155,24 @@ function resolveZalouserInboundSessionKey(params: {
147
155
  return params.route.sessionKey;
148
156
  }
149
157
 
150
- const directSessionKey = params.core.channel.routing
151
- .buildAgentSessionKey({
158
+ const directSessionKey = normalizeLowercaseStringOrEmpty(
159
+ params.core.channel.routing.buildAgentSessionKey({
152
160
  agentId: params.route.agentId,
153
161
  channel: "zalouser",
154
162
  accountId: params.route.accountId,
155
163
  peer: { kind: "direct", id: params.senderId },
156
164
  dmScope: resolveZalouserDmSessionScope(params.config),
157
165
  identityLinks: params.config.session?.identityLinks,
158
- })
159
- .toLowerCase();
160
- const legacySessionKey = params.core.channel.routing
161
- .buildAgentSessionKey({
166
+ }),
167
+ );
168
+ const legacySessionKey = normalizeLowercaseStringOrEmpty(
169
+ params.core.channel.routing.buildAgentSessionKey({
162
170
  agentId: params.route.agentId,
163
171
  channel: "zalouser",
164
172
  accountId: params.route.accountId,
165
173
  peer: { kind: "group", id: params.senderId },
166
- })
167
- .toLowerCase();
174
+ }),
175
+ );
168
176
  const hasDirectSession =
169
177
  params.core.channel.session.readSessionUpdatedAt({
170
178
  storePath: params.storePath,
@@ -190,12 +198,12 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
190
198
  if (allowFrom.includes("*")) {
191
199
  return true;
192
200
  }
193
- const normalizedSenderId = senderId?.trim().toLowerCase();
201
+ const normalizedSenderId = normalizeOptionalLowercaseString(senderId);
194
202
  if (!normalizedSenderId) {
195
203
  return false;
196
204
  }
197
205
  return allowFrom.some((entry) => {
198
- const normalized = entry.toLowerCase().replace(/^(zalouser|zlu):/i, "");
206
+ const normalized = normalizeLowercaseStringOrEmpty(entry).replace(/^(zalouser|zlu):/i, "");
199
207
  return normalized === normalizedSenderId;
200
208
  });
201
209
  }
@@ -203,7 +211,7 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
203
211
  function resolveGroupRequireMention(params: {
204
212
  groupId: string;
205
213
  groupName?: string | null;
206
- groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
214
+ groups: Record<string, { enabled?: boolean; requireMention?: boolean }>;
207
215
  allowNameMatching?: boolean;
208
216
  }): boolean {
209
217
  const entry = findZalouserGroupEntry(
@@ -249,7 +257,7 @@ async function processMessage(
249
257
  historyState: ZalouserGroupHistoryState,
250
258
  statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
251
259
  ): Promise<void> {
252
- const pairing = createScopedPairingAccess({
260
+ const pairing = createChannelPairingController({
253
261
  core,
254
262
  channel: "zalouser",
255
263
  accountId: account.accountId,
@@ -310,6 +318,7 @@ async function processMessage(
310
318
  });
311
319
 
312
320
  const groups = account.config.groups ?? {};
321
+ const routeAllowlistConfigured = Object.keys(groups).length > 0;
313
322
  const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
314
323
  if (isGroup) {
315
324
  const groupEntry = findZalouserGroupEntry(
@@ -324,7 +333,7 @@ async function processMessage(
324
333
  );
325
334
  const routeAccess = evaluateGroupRouteAccessForPolicy({
326
335
  groupPolicy,
327
- routeAllowlistConfigured: Object.keys(groups).length > 0,
336
+ routeAllowlistConfigured,
328
337
  routeMatched: Boolean(groupEntry),
329
338
  routeEnabled: isZalouserGroupEntryAllowed(groupEntry),
330
339
  });
@@ -349,21 +358,25 @@ async function processMessage(
349
358
  const dmPolicy = account.config.dmPolicy ?? "pairing";
350
359
  const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
351
360
  const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((v) => String(v));
352
- const shouldComputeCommandAuth = core.channel.commands.shouldComputeCommandAuthorized(
353
- commandBody,
354
- config,
355
- );
361
+ const senderGroupPolicy =
362
+ routeAllowlistConfigured && configGroupAllowFrom.length === 0
363
+ ? groupPolicy
364
+ : resolveSenderScopedGroupPolicy({
365
+ groupPolicy,
366
+ groupAllowFrom: configGroupAllowFrom,
367
+ });
356
368
  const storeAllowFrom =
357
- !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeCommandAuth)
369
+ !isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open"
358
370
  ? await pairing.readAllowFromStore().catch(() => [])
359
371
  : [];
360
372
  const accessDecision = resolveDmGroupAccessWithLists({
361
373
  isGroup,
362
374
  dmPolicy,
363
- groupPolicy,
375
+ groupPolicy: senderGroupPolicy,
364
376
  allowFrom: configAllowFrom,
365
377
  groupAllowFrom: configGroupAllowFrom,
366
378
  storeAllowFrom,
379
+ groupAllowFromFallbackToAllowFrom: false,
367
380
  isSenderAllowed: (allowFrom) => isSenderAllowed(senderId, allowFrom),
368
381
  });
369
382
  if (isGroup && accessDecision.decision !== "allow") {
@@ -381,12 +394,10 @@ async function processMessage(
381
394
 
382
395
  if (!isGroup && accessDecision.decision !== "allow") {
383
396
  if (accessDecision.decision === "pairing") {
384
- await issuePairingChallenge({
385
- channel: "zalouser",
397
+ await pairing.issueChallenge({
386
398
  senderId,
387
399
  senderIdLine: `Your Zalo user id: ${senderId}`,
388
400
  meta: { name: senderName || undefined },
389
- upsertPairingRequest: pairing.upsertPairingRequest,
390
401
  onCreated: () => {
391
402
  logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
392
403
  },
@@ -425,6 +436,8 @@ async function processMessage(
425
436
  configuredGroupAllowFrom: configGroupAllowFrom,
426
437
  senderId,
427
438
  isSenderAllowed,
439
+ channel: "zalouser",
440
+ accountId: account.accountId,
428
441
  readAllowFromStore: async () => storeAllowFrom,
429
442
  shouldComputeCommandAuthorized: (body, cfg) =>
430
443
  core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
@@ -479,28 +492,32 @@ async function processMessage(
479
492
  })
480
493
  : true;
481
494
  const canDetectMention = mentionRegexes.length > 0 || explicitMention.canResolveExplicit;
482
- const mentionGate = resolveMentionGatingWithBypass({
483
- isGroup,
484
- requireMention,
485
- canDetectMention,
486
- wasMentioned,
487
- implicitMention: message.implicitMention === true,
488
- hasAnyMention: explicitMention.hasAnyMention,
489
- allowTextCommands: core.channel.commands.shouldHandleTextCommands({
490
- cfg: config,
491
- surface: "zalouser",
492
- }),
493
- hasControlCommand,
494
- commandAuthorized: commandAuthorized === true,
495
+ const mentionDecision = resolveInboundMentionDecision({
496
+ facts: {
497
+ canDetectMention,
498
+ wasMentioned,
499
+ hasAnyMention: explicitMention.hasAnyMention,
500
+ implicitMentionKinds: implicitMentionKindWhen("quoted_bot", message.implicitMention === true),
501
+ },
502
+ policy: {
503
+ isGroup,
504
+ requireMention,
505
+ allowTextCommands: core.channel.commands.shouldHandleTextCommands({
506
+ cfg: config,
507
+ surface: "zalouser",
508
+ }),
509
+ hasControlCommand,
510
+ commandAuthorized: commandAuthorized === true,
511
+ },
495
512
  });
496
- if (isGroup && requireMention && !canDetectMention && !mentionGate.effectiveWasMentioned) {
513
+ if (isGroup && requireMention && !canDetectMention && !mentionDecision.effectiveWasMentioned) {
497
514
  runtime.error?.(
498
515
  `[${account.accountId}] zalouser mention required but detection unavailable ` +
499
516
  `(missing mention regexes and bot self id); dropping group ${chatId}`,
500
517
  );
501
518
  return;
502
519
  }
503
- if (isGroup && mentionGate.shouldSkip) {
520
+ if (isGroup && mentionDecision.shouldSkip) {
504
521
  recordPendingHistoryEntryIfEnabled({
505
522
  historyMap: historyState.groupHistories,
506
523
  historyKey: historyKey ?? "",
@@ -577,102 +594,144 @@ async function processMessage(
577
594
  : undefined;
578
595
 
579
596
  const normalizedTo = isGroup ? `zalouser:group:${chatId}` : `zalouser:${chatId}`;
580
-
581
- const ctxPayload = core.channel.reply.finalizeInboundContext({
582
- Body: combinedBody,
583
- BodyForAgent: rawBody,
584
- InboundHistory: inboundHistory,
585
- RawBody: rawBody,
586
- CommandBody: commandBody,
587
- BodyForCommands: commandBody,
588
- From: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
589
- To: normalizedTo,
590
- SessionKey: inboundSessionKey,
591
- AccountId: route.accountId,
592
- ChatType: isGroup ? "group" : "direct",
593
- ConversationLabel: fromLabel,
594
- GroupSubject: isGroup ? groupName || undefined : undefined,
595
- GroupChannel: isGroup ? groupName || undefined : undefined,
596
- GroupMembers: isGroup ? groupMembers : undefined,
597
- SenderName: senderName || undefined,
598
- SenderId: senderId,
599
- WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : undefined,
600
- CommandAuthorized: commandAuthorized,
601
- Provider: "zalouser",
602
- Surface: "zalouser",
603
- MessageSid: resolveZalouserMessageSid({
604
- msgId: message.msgId,
605
- cliMsgId: message.cliMsgId,
606
- fallback: `${message.timestampMs}`,
607
- }),
608
- MessageSidFull: formatZalouserMessageSidFull({
609
- msgId: message.msgId,
610
- cliMsgId: message.cliMsgId,
611
- }),
612
- OriginatingChannel: "zalouser",
613
- OriginatingTo: normalizedTo,
597
+ const messageSid = resolveZalouserMessageSid({
598
+ msgId: message.msgId,
599
+ cliMsgId: message.cliMsgId,
600
+ fallback: `${message.timestampMs}`,
614
601
  });
615
-
616
- await core.channel.session.recordInboundSession({
617
- storePath,
618
- sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
619
- ctx: ctxPayload,
620
- onRecordError: (err) => {
621
- runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
622
- },
602
+ const messageSidFull = formatZalouserMessageSidFull({
603
+ msgId: message.msgId,
604
+ cliMsgId: message.cliMsgId,
623
605
  });
624
606
 
625
- const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
626
- cfg: config,
627
- agentId: route.agentId,
607
+ const ctxPayload = core.channel.turn.buildContext({
628
608
  channel: "zalouser",
629
- accountId: account.accountId,
630
- });
631
- const typingCallbacks = createTypingCallbacks({
632
- start: async () => {
633
- await sendTypingZalouser(chatId, {
634
- profile: account.profile,
635
- isGroup,
636
- });
609
+ accountId: route.accountId,
610
+ messageId: messageSid,
611
+ messageIdFull: messageSidFull,
612
+ timestamp: message.timestampMs,
613
+ from: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
614
+ sender: {
615
+ id: senderId,
616
+ name: senderName || undefined,
637
617
  },
638
- onStartError: (err) => {
639
- runtime.error?.(
640
- `[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`,
641
- );
642
- logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
618
+ conversation: {
619
+ kind: isGroup ? "group" : "direct",
620
+ id: chatId,
621
+ label: fromLabel,
622
+ routePeer: {
623
+ kind: isGroup ? "group" : "direct",
624
+ id: chatId,
625
+ },
626
+ },
627
+ route: {
628
+ agentId: route.agentId,
629
+ accountId: route.accountId,
630
+ routeSessionKey: route.sessionKey,
631
+ dispatchSessionKey: inboundSessionKey,
632
+ },
633
+ reply: {
634
+ to: normalizedTo,
635
+ originatingTo: normalizedTo,
636
+ },
637
+ message: {
638
+ body: combinedBody,
639
+ bodyForAgent: rawBody,
640
+ rawBody,
641
+ commandBody,
642
+ inboundHistory,
643
+ envelopeFrom: fromLabel,
644
+ },
645
+ extra: {
646
+ BodyForCommands: commandBody,
647
+ GroupSubject: isGroup ? groupName || undefined : undefined,
648
+ GroupChannel: isGroup ? groupName || undefined : undefined,
649
+ GroupMembers: isGroup ? groupMembers : undefined,
650
+ WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : undefined,
651
+ CommandAuthorized: commandAuthorized,
643
652
  },
644
653
  });
645
654
 
646
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
647
- ctx: ctxPayload,
655
+ const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
648
656
  cfg: config,
649
- dispatcherOptions: {
650
- ...prefixOptions,
651
- typingCallbacks,
652
- deliver: async (payload) => {
653
- await deliverZalouserReply({
654
- payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
657
+ agentId: route.agentId,
658
+ channel: "zalouser",
659
+ accountId: account.accountId,
660
+ typing: {
661
+ start: async () => {
662
+ await sendTypingZalouser(chatId, {
655
663
  profile: account.profile,
656
- chatId,
657
664
  isGroup,
658
- runtime,
659
- core,
660
- config,
661
- accountId: account.accountId,
662
- statusSink,
663
- tableMode: core.channel.text.resolveMarkdownTableMode({
664
- cfg: config,
665
- channel: "zalouser",
666
- accountId: account.accountId,
667
- }),
668
665
  });
669
666
  },
670
- onError: (err, info) => {
671
- runtime.error(`[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`);
667
+ onStartError: (err) => {
668
+ runtime.error?.(
669
+ `[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`,
670
+ );
671
+ logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
672
672
  },
673
673
  },
674
- replyOptions: {
675
- onModelSelected,
674
+ });
675
+
676
+ await core.channel.turn.run({
677
+ channel: "zalouser",
678
+ accountId: account.accountId,
679
+ raw: message,
680
+ adapter: {
681
+ ingest: () => ({
682
+ id: messageSid ?? `${message.timestampMs}`,
683
+ timestamp: message.timestampMs,
684
+ rawText: rawBody,
685
+ textForAgent: rawBody,
686
+ textForCommands: commandBody,
687
+ raw: message,
688
+ }),
689
+ resolveTurn: () => ({
690
+ cfg: config,
691
+ channel: "zalouser",
692
+ accountId: account.accountId,
693
+ agentId: route.agentId,
694
+ routeSessionKey: route.sessionKey,
695
+ storePath,
696
+ ctxPayload,
697
+ recordInboundSession: core.channel.session.recordInboundSession,
698
+ dispatchReplyWithBufferedBlockDispatcher:
699
+ core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
700
+ delivery: {
701
+ deliver: async (payload) => {
702
+ await deliverZalouserReply({
703
+ payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
704
+ profile: account.profile,
705
+ chatId,
706
+ isGroup,
707
+ runtime,
708
+ core,
709
+ config,
710
+ accountId: account.accountId,
711
+ statusSink,
712
+ tableMode: core.channel.text.resolveMarkdownTableMode({
713
+ cfg: config,
714
+ channel: "zalouser",
715
+ accountId: account.accountId,
716
+ }),
717
+ });
718
+ },
719
+ onError: (err, info) => {
720
+ runtime.error(
721
+ `[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`,
722
+ );
723
+ },
724
+ },
725
+ dispatcherOptions: replyPipeline,
726
+ replyOptions: {
727
+ onModelSelected,
728
+ },
729
+ record: {
730
+ onRecordError: (err) => {
731
+ runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
732
+ },
733
+ },
734
+ }),
676
735
  },
677
736
  });
678
737
  if (isGroup && historyKey) {
@@ -699,16 +758,31 @@ async function deliverZalouserReply(params: {
699
758
  const { payload, profile, chatId, isGroup, runtime, core, config, accountId, statusSink } =
700
759
  params;
701
760
  const tableMode = params.tableMode ?? "code";
702
- const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
761
+ const reply = resolveSendableOutboundReplyParts(payload, {
762
+ text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
763
+ });
703
764
  const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
704
765
  const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, {
705
766
  fallbackLimit: ZALOUSER_TEXT_LIMIT,
706
767
  });
707
-
708
- const sentMedia = await sendMediaWithLeadingCaption({
709
- mediaUrls: resolveOutboundMediaUrls(payload),
710
- caption: text,
711
- send: async ({ mediaUrl, caption }) => {
768
+ await deliverTextOrMediaReply({
769
+ payload,
770
+ text: reply.text,
771
+ sendText: async (chunk) => {
772
+ try {
773
+ await sendMessageZalouser(chatId, chunk, {
774
+ profile,
775
+ isGroup,
776
+ textMode: "markdown",
777
+ textChunkMode: chunkMode,
778
+ textChunkLimit,
779
+ });
780
+ statusSink?.({ lastOutboundAt: Date.now() });
781
+ } catch (err) {
782
+ runtime.error(`Zalouser message send failed: ${String(err)}`);
783
+ }
784
+ },
785
+ sendMedia: async ({ mediaUrl, caption }) => {
712
786
  logVerbose(core, runtime, `Sending media to ${chatId}`);
713
787
  await sendMessageZalouser(chatId, caption ?? "", {
714
788
  profile,
@@ -720,28 +794,14 @@ async function deliverZalouserReply(params: {
720
794
  });
721
795
  statusSink?.({ lastOutboundAt: Date.now() });
722
796
  },
723
- onError: (error) => {
724
- runtime.error(`Zalouser media send failed: ${String(error)}`);
797
+ onMediaError: (error) => {
798
+ runtime.error(
799
+ `Zalouser media send failed: ${
800
+ error instanceof Error ? error.message : JSON.stringify(error)
801
+ }`,
802
+ );
725
803
  },
726
804
  });
727
- if (sentMedia) {
728
- return;
729
- }
730
-
731
- if (text) {
732
- try {
733
- await sendMessageZalouser(chatId, text, {
734
- profile,
735
- isGroup,
736
- textMode: "markdown",
737
- textChunkMode: chunkMode,
738
- textChunkLimit,
739
- });
740
- statusSink?.({ lastOutboundAt: Date.now() });
741
- } catch (err) {
742
- runtime.error(`Zalouser message send failed: ${String(err)}`);
743
- }
744
- }
745
805
  }
746
806
 
747
807
  export async function monitorZalouserProvider(
@@ -824,9 +884,9 @@ export async function monitorZalouserProvider(
824
884
  mapping.push(`${entry}→${cleaned}`);
825
885
  continue;
826
886
  }
827
- const matches = byName.get(cleaned.toLowerCase()) ?? [];
887
+ const matches = byName.get(normalizeLowercaseStringOrEmpty(cleaned)) ?? [];
828
888
  const match = matches[0];
829
- const id = match?.groupId ? String(match.groupId) : undefined;
889
+ const id = match?.groupId;
830
890
  if (id) {
831
891
  if (!nextGroups[id]) {
832
892
  nextGroups[id] = groupsConfig[entry];
package/src/probe.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { BaseProbeResult } from "openclaw/plugin-sdk/zalouser";
1
+ import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
2
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
2
3
  import type { ZcaUserInfo } from "./types.js";
3
4
  import { getZaloUserInfo } from "./zalo-js.js";
4
5
 
@@ -28,7 +29,7 @@ export async function probeZalouser(
28
29
  } catch (error) {
29
30
  return {
30
31
  ok: false,
31
- error: error instanceof Error ? error.message : String(error),
32
+ error: formatErrorMessage(error),
32
33
  };
33
34
  }
34
35
  }
@@ -1,6 +1,6 @@
1
1
  import fsp from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/zalouser";
3
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
4
4
 
5
5
  export async function writeQrDataUrlToTempFile(
6
6
  qrDataUrl: string,
package/src/reaction.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Reactions } from "./zca-client.js";
1
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
2
+ import { Reactions } from "./zca-constants.js";
2
3
 
3
4
  const REACTION_ALIAS_MAP = new Map<string, string>([
4
5
  ["like", Reactions.LIKE],
@@ -24,6 +25,8 @@ export function normalizeZaloReactionIcon(raw: string): string {
24
25
  return Reactions.LIKE;
25
26
  }
26
27
  return (
27
- REACTION_ALIAS_MAP.get(trimmed.toLowerCase()) ?? REACTION_ALIAS_MAP.get(trimmed) ?? trimmed
28
+ REACTION_ALIAS_MAP.get(normalizeLowercaseStringOrEmpty(trimmed)) ??
29
+ REACTION_ALIAS_MAP.get(trimmed) ??
30
+ trimmed
28
31
  );
29
32
  }
package/src/runtime.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
2
- import type { PluginRuntime } from "openclaw/plugin-sdk/zalouser";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
2
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
3
3
 
4
4
  const { setRuntime: setZalouserRuntime, getRuntime: getZalouserRuntime } =
5
- createPluginRuntimeStore<PluginRuntime>("Zalouser runtime not initialized");
5
+ createPluginRuntimeStore<PluginRuntime>({
6
+ pluginId: "zalouser",
7
+ errorMessage: "Zalouser runtime not initialized",
8
+ });
6
9
  export { getZalouserRuntime, setZalouserRuntime };