@openclaw/zalouser 2026.5.2 → 2026.5.3-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 (95) hide show
  1. package/dist/accounts-C00IMUgd.js +63 -0
  2. package/dist/accounts.runtime-uG7S8cXT.js +2 -0
  3. package/dist/api-BRwdUWuS.js +139 -0
  4. package/dist/api.js +7 -0
  5. package/dist/channel-ou_w_2j-.js +484 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-C9WxiAiR.js +25 -0
  8. package/dist/channel.setup-CiDeBFrn.js +10 -0
  9. package/dist/contract-api.js +3 -0
  10. package/dist/doctor-contract-DgqHp8E2.js +128 -0
  11. package/dist/doctor-contract-api.js +2 -0
  12. package/dist/index.js +27 -0
  13. package/dist/monitor-Cg7K_s_s.js +705 -0
  14. package/dist/runtime-QNU7vLgI.js +106 -0
  15. package/dist/runtime-api.js +22 -0
  16. package/dist/secret-contract-api.js +5 -0
  17. package/dist/security-audit-BZLhil-V.js +34 -0
  18. package/dist/send-BsmySxe3.js +534 -0
  19. package/dist/session-route-C0-Xr8bt.js +92 -0
  20. package/dist/setup-core-CqipqY98.js +40 -0
  21. package/dist/setup-entry.js +11 -0
  22. package/dist/setup-plugin-api.js +2 -0
  23. package/dist/setup-surface-NCOuKu-l.js +359 -0
  24. package/dist/shared-DSy8aIUx.js +120 -0
  25. package/dist/test-api.js +5 -0
  26. package/dist/zalo-js-CHCUlY3c.js +1279 -0
  27. package/package.json +15 -6
  28. package/api.ts +0 -9
  29. package/channel-plugin-api.ts +0 -3
  30. package/contract-api.ts +0 -2
  31. package/doctor-contract-api.ts +0 -1
  32. package/index.ts +0 -34
  33. package/runtime-api.ts +0 -67
  34. package/secret-contract-api.ts +0 -4
  35. package/setup-entry.ts +0 -9
  36. package/setup-plugin-api.ts +0 -2
  37. package/src/accounts.runtime.ts +0 -1
  38. package/src/accounts.test-mocks.ts +0 -14
  39. package/src/accounts.test.ts +0 -266
  40. package/src/accounts.ts +0 -131
  41. package/src/channel-api.ts +0 -20
  42. package/src/channel.adapters.ts +0 -391
  43. package/src/channel.directory.test.ts +0 -59
  44. package/src/channel.runtime.ts +0 -12
  45. package/src/channel.sendpayload.test.ts +0 -172
  46. package/src/channel.setup.test.ts +0 -33
  47. package/src/channel.setup.ts +0 -12
  48. package/src/channel.test.ts +0 -377
  49. package/src/channel.ts +0 -219
  50. package/src/config-schema.ts +0 -33
  51. package/src/directory.ts +0 -54
  52. package/src/doctor-contract.ts +0 -156
  53. package/src/doctor.test.ts +0 -77
  54. package/src/doctor.ts +0 -37
  55. package/src/group-policy.test.ts +0 -61
  56. package/src/group-policy.ts +0 -83
  57. package/src/message-sid.test.ts +0 -66
  58. package/src/message-sid.ts +0 -80
  59. package/src/monitor.account-scope.test.ts +0 -107
  60. package/src/monitor.group-gating.test.ts +0 -816
  61. package/src/monitor.send-mocks.ts +0 -20
  62. package/src/monitor.ts +0 -1044
  63. package/src/probe.test.ts +0 -60
  64. package/src/probe.ts +0 -35
  65. package/src/qr-temp-file.ts +0 -22
  66. package/src/reaction.test.ts +0 -19
  67. package/src/reaction.ts +0 -32
  68. package/src/runtime.ts +0 -9
  69. package/src/security-audit.test.ts +0 -80
  70. package/src/security-audit.ts +0 -71
  71. package/src/send.test.ts +0 -395
  72. package/src/send.ts +0 -272
  73. package/src/session-route.ts +0 -121
  74. package/src/setup-core.ts +0 -33
  75. package/src/setup-surface.test.ts +0 -363
  76. package/src/setup-surface.ts +0 -470
  77. package/src/setup-test-helpers.ts +0 -42
  78. package/src/shared.ts +0 -92
  79. package/src/status-issues.test.ts +0 -31
  80. package/src/status-issues.ts +0 -58
  81. package/src/test-helpers.ts +0 -26
  82. package/src/text-styles.test.ts +0 -203
  83. package/src/text-styles.ts +0 -540
  84. package/src/tool.test.ts +0 -212
  85. package/src/tool.ts +0 -210
  86. package/src/types.ts +0 -125
  87. package/src/zalo-js.credentials.test.ts +0 -465
  88. package/src/zalo-js.test-mocks.ts +0 -89
  89. package/src/zalo-js.ts +0 -1911
  90. package/src/zca-client.test.ts +0 -24
  91. package/src/zca-client.ts +0 -259
  92. package/src/zca-constants.ts +0 -55
  93. package/src/zca-js-exports.d.ts +0 -22
  94. package/test-api.ts +0 -21
  95. package/tsconfig.json +0 -16
@@ -0,0 +1,705 @@
1
+ import { c as isZalouserGroupEntryAllowed, i as resolveZalouserMessageSid, o as buildZalouserGroupCandidates, r as formatZalouserMessageSidFull, s as findZalouserGroupEntry, t as getZalouserRuntime } from "./runtime-QNU7vLgI.js";
2
+ import { o as listZaloGroups, r as listZaloFriends, u as resolveZaloGroupContext, v as startZaloListener } from "./zalo-js-CHCUlY3c.js";
3
+ import { i as sendMessageZalouser, o as sendSeenZalouser, s as sendTypingZalouser, t as sendDeliveredZalouser } from "./send-BsmySxe3.js";
4
+ import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
5
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
6
+ import { mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
7
+ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core";
8
+ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
9
+ import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
10
+ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
11
+ import { DM_GROUP_ACCESS_REASON, resolveDmGroupAccessWithLists } from "openclaw/plugin-sdk/channel-policy";
12
+ import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
13
+ import { implicitMentionKindWhen, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
14
+ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
15
+ import { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
16
+ import { evaluateGroupRouteAccessForPolicy, resolveSenderScopedGroupPolicy } from "openclaw/plugin-sdk/group-access";
17
+ import { DEFAULT_GROUP_HISTORY_LIMIT, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
18
+ //#region extensions/zalouser/src/monitor.ts
19
+ const ZALOUSER_TEXT_LIMIT = 2e3;
20
+ function normalizeZalouserEntry(entry) {
21
+ return entry.replace(/^(zalouser|zlu):/i, "").trim();
22
+ }
23
+ function buildNameIndex(items, nameFn) {
24
+ const index = /* @__PURE__ */ new Map();
25
+ for (const item of items) {
26
+ const name = normalizeOptionalLowercaseString(nameFn(item));
27
+ if (!name) continue;
28
+ const list = index.get(name) ?? [];
29
+ list.push(item);
30
+ index.set(name, list);
31
+ }
32
+ return index;
33
+ }
34
+ function resolveUserAllowlistEntries(entries, byName) {
35
+ const additions = [];
36
+ const mapping = [];
37
+ const unresolved = [];
38
+ for (const entry of entries) {
39
+ if (/^\d+$/.test(entry)) {
40
+ additions.push(entry);
41
+ continue;
42
+ }
43
+ const id = (byName.get(normalizeLowercaseStringOrEmpty(entry)) ?? [])[0]?.userId;
44
+ if (id) {
45
+ additions.push(id);
46
+ mapping.push(`${entry}->${id}`);
47
+ } else unresolved.push(entry);
48
+ }
49
+ return {
50
+ additions,
51
+ mapping,
52
+ unresolved
53
+ };
54
+ }
55
+ function resolveInboundQueueKey(message) {
56
+ const threadId = message.threadId?.trim() || "unknown";
57
+ if (message.isGroup) return `group:${threadId}`;
58
+ return `direct:${message.senderId?.trim() || threadId}`;
59
+ }
60
+ function resolveZalouserDmSessionScope(config) {
61
+ const configured = config.session?.dmScope;
62
+ return configured === "main" || !configured ? "per-channel-peer" : configured;
63
+ }
64
+ function resolveZalouserInboundSessionKey(params) {
65
+ if (params.isGroup) return params.route.sessionKey;
66
+ const directSessionKey = normalizeLowercaseStringOrEmpty(params.core.channel.routing.buildAgentSessionKey({
67
+ agentId: params.route.agentId,
68
+ channel: "zalouser",
69
+ accountId: params.route.accountId,
70
+ peer: {
71
+ kind: "direct",
72
+ id: params.senderId
73
+ },
74
+ dmScope: resolveZalouserDmSessionScope(params.config),
75
+ identityLinks: params.config.session?.identityLinks
76
+ }));
77
+ const legacySessionKey = normalizeLowercaseStringOrEmpty(params.core.channel.routing.buildAgentSessionKey({
78
+ agentId: params.route.agentId,
79
+ channel: "zalouser",
80
+ accountId: params.route.accountId,
81
+ peer: {
82
+ kind: "group",
83
+ id: params.senderId
84
+ }
85
+ }));
86
+ const hasDirectSession = params.core.channel.session.readSessionUpdatedAt({
87
+ storePath: params.storePath,
88
+ sessionKey: directSessionKey
89
+ }) !== void 0;
90
+ return params.core.channel.session.readSessionUpdatedAt({
91
+ storePath: params.storePath,
92
+ sessionKey: legacySessionKey
93
+ }) !== void 0 && !hasDirectSession ? legacySessionKey : directSessionKey;
94
+ }
95
+ function logVerbose(core, runtime, message) {
96
+ if (core.logging.shouldLogVerbose()) runtime.log(`[zalouser] ${message}`);
97
+ }
98
+ function isSenderAllowed(senderId, allowFrom) {
99
+ if (allowFrom.includes("*")) return true;
100
+ const normalizedSenderId = normalizeOptionalLowercaseString(senderId);
101
+ if (!normalizedSenderId) return false;
102
+ return allowFrom.some((entry) => {
103
+ return normalizeLowercaseStringOrEmpty(entry).replace(/^(zalouser|zlu):/i, "") === normalizedSenderId;
104
+ });
105
+ }
106
+ function resolveGroupRequireMention(params) {
107
+ const entry = findZalouserGroupEntry(params.groups ?? {}, buildZalouserGroupCandidates({
108
+ groupId: params.groupId,
109
+ groupName: params.groupName,
110
+ includeGroupIdAlias: true,
111
+ includeWildcard: true,
112
+ allowNameMatching: params.allowNameMatching
113
+ }));
114
+ if (typeof entry?.requireMention === "boolean") return entry.requireMention;
115
+ return true;
116
+ }
117
+ async function sendZalouserDeliveryAcks(params) {
118
+ await sendDeliveredZalouser({
119
+ profile: params.profile,
120
+ isGroup: params.isGroup,
121
+ message: params.message,
122
+ isSeen: true
123
+ });
124
+ await sendSeenZalouser({
125
+ profile: params.profile,
126
+ isGroup: params.isGroup,
127
+ message: params.message
128
+ });
129
+ }
130
+ async function processMessage(message, account, config, core, runtime, historyState, statusSink) {
131
+ const pairing = createChannelPairingController({
132
+ core,
133
+ channel: "zalouser",
134
+ accountId: account.accountId
135
+ });
136
+ const rawBody = message.content?.trim();
137
+ if (!rawBody) return;
138
+ const commandBody = message.commandContent?.trim() || rawBody;
139
+ const isGroup = message.isGroup;
140
+ const chatId = message.threadId;
141
+ const senderId = message.senderId?.trim();
142
+ if (!senderId) {
143
+ logVerbose(core, runtime, `zalouser: drop message ${chatId} (missing senderId)`);
144
+ return;
145
+ }
146
+ const senderName = message.senderName ?? "";
147
+ const configuredGroupName = message.groupName?.trim() || "";
148
+ const groupContext = isGroup && !configuredGroupName ? await resolveZaloGroupContext(account.profile, chatId).catch((err) => {
149
+ logVerbose(core, runtime, `zalouser: group context lookup failed for ${chatId}: ${String(err)}`);
150
+ return null;
151
+ }) : null;
152
+ const groupName = configuredGroupName || groupContext?.name?.trim() || "";
153
+ const groupMembers = groupContext?.members?.slice(0, 20).join(", ") || void 0;
154
+ if (message.eventMessage) try {
155
+ await sendZalouserDeliveryAcks({
156
+ profile: account.profile,
157
+ isGroup,
158
+ message: message.eventMessage
159
+ });
160
+ } catch (err) {
161
+ logVerbose(core, runtime, `zalouser: delivery/seen ack failed for ${chatId}: ${String(err)}`);
162
+ }
163
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
164
+ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
165
+ providerConfigPresent: config.channels?.zalouser !== void 0,
166
+ groupPolicy: account.config.groupPolicy,
167
+ defaultGroupPolicy
168
+ });
169
+ warnMissingProviderGroupPolicyFallbackOnce({
170
+ providerMissingFallbackApplied,
171
+ providerKey: "zalouser",
172
+ accountId: account.accountId,
173
+ log: (entry) => logVerbose(core, runtime, entry)
174
+ });
175
+ const groups = account.config.groups ?? {};
176
+ const routeAllowlistConfigured = Object.keys(groups).length > 0;
177
+ const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
178
+ if (isGroup) {
179
+ const groupEntry = findZalouserGroupEntry(groups, buildZalouserGroupCandidates({
180
+ groupId: chatId,
181
+ groupName,
182
+ includeGroupIdAlias: true,
183
+ includeWildcard: true,
184
+ allowNameMatching
185
+ }));
186
+ const routeAccess = evaluateGroupRouteAccessForPolicy({
187
+ groupPolicy,
188
+ routeAllowlistConfigured,
189
+ routeMatched: Boolean(groupEntry),
190
+ routeEnabled: isZalouserGroupEntryAllowed(groupEntry)
191
+ });
192
+ if (!routeAccess.allowed) {
193
+ if (routeAccess.reason === "disabled") logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
194
+ else if (routeAccess.reason === "empty_allowlist") logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=allowlist, no allowlist)`);
195
+ else if (routeAccess.reason === "route_not_allowlisted") logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
196
+ else if (routeAccess.reason === "route_disabled") logVerbose(core, runtime, `zalouser: drop group ${chatId} (group disabled)`);
197
+ return;
198
+ }
199
+ }
200
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
201
+ const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
202
+ const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((v) => String(v));
203
+ const senderGroupPolicy = routeAllowlistConfigured && configGroupAllowFrom.length === 0 ? groupPolicy : resolveSenderScopedGroupPolicy({
204
+ groupPolicy,
205
+ groupAllowFrom: configGroupAllowFrom
206
+ });
207
+ const storeAllowFrom = !isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open" ? await pairing.readAllowFromStore().catch(() => []) : [];
208
+ const accessDecision = resolveDmGroupAccessWithLists({
209
+ isGroup,
210
+ dmPolicy,
211
+ groupPolicy: senderGroupPolicy,
212
+ allowFrom: configAllowFrom,
213
+ groupAllowFrom: configGroupAllowFrom,
214
+ storeAllowFrom,
215
+ groupAllowFromFallbackToAllowFrom: false,
216
+ isSenderAllowed: (allowFrom) => isSenderAllowed(senderId, allowFrom)
217
+ });
218
+ if (isGroup && accessDecision.decision !== "allow") {
219
+ if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) logVerbose(core, runtime, "Blocked zalouser group message (no group allowlist)");
220
+ else if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED) logVerbose(core, runtime, `Blocked zalouser sender ${senderId} (not in groupAllowFrom/allowFrom)`);
221
+ return;
222
+ }
223
+ if (!isGroup && accessDecision.decision !== "allow") {
224
+ if (accessDecision.decision === "pairing") {
225
+ await pairing.issueChallenge({
226
+ senderId,
227
+ senderIdLine: `Your Zalo user id: ${senderId}`,
228
+ meta: { name: senderName || void 0 },
229
+ onCreated: () => {
230
+ logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
231
+ },
232
+ sendPairingReply: async (text) => {
233
+ await sendMessageZalouser(chatId, text, { profile: account.profile });
234
+ statusSink?.({ lastOutboundAt: Date.now() });
235
+ },
236
+ onReplyError: (err) => {
237
+ logVerbose(core, runtime, `zalouser pairing reply failed for ${senderId}: ${String(err)}`);
238
+ }
239
+ });
240
+ return;
241
+ }
242
+ if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED) logVerbose(core, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
243
+ else logVerbose(core, runtime, `Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`);
244
+ return;
245
+ }
246
+ const { commandAuthorized } = await resolveSenderCommandAuthorization({
247
+ cfg: config,
248
+ rawBody: commandBody,
249
+ isGroup,
250
+ dmPolicy,
251
+ configuredAllowFrom: configAllowFrom,
252
+ configuredGroupAllowFrom: configGroupAllowFrom,
253
+ senderId,
254
+ isSenderAllowed,
255
+ channel: "zalouser",
256
+ accountId: account.accountId,
257
+ readAllowFromStore: async () => storeAllowFrom,
258
+ shouldComputeCommandAuthorized: (body, cfg) => core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
259
+ resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params)
260
+ });
261
+ const hasControlCommand = core.channel.commands.isControlCommandMessage(commandBody, config);
262
+ if (isGroup && hasControlCommand && commandAuthorized !== true) {
263
+ logVerbose(core, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`);
264
+ return;
265
+ }
266
+ const peer = isGroup ? {
267
+ kind: "group",
268
+ id: chatId
269
+ } : {
270
+ kind: "direct",
271
+ id: senderId
272
+ };
273
+ const route = core.channel.routing.resolveAgentRoute({
274
+ cfg: config,
275
+ channel: "zalouser",
276
+ accountId: account.accountId,
277
+ peer: {
278
+ kind: peer.kind,
279
+ id: peer.id
280
+ }
281
+ });
282
+ const historyKey = isGroup ? route.sessionKey : void 0;
283
+ const requireMention = isGroup ? resolveGroupRequireMention({
284
+ groupId: chatId,
285
+ groupName,
286
+ groups,
287
+ allowNameMatching
288
+ }) : false;
289
+ const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
290
+ const explicitMention = {
291
+ hasAnyMention: message.hasAnyMention === true,
292
+ isExplicitlyMentioned: message.wasExplicitlyMentioned === true,
293
+ canResolveExplicit: message.canResolveExplicitMention === true
294
+ };
295
+ const wasMentioned = isGroup ? core.channel.mentions.matchesMentionWithExplicit({
296
+ text: rawBody,
297
+ mentionRegexes,
298
+ explicit: explicitMention
299
+ }) : true;
300
+ const canDetectMention = mentionRegexes.length > 0 || explicitMention.canResolveExplicit;
301
+ const mentionDecision = resolveInboundMentionDecision({
302
+ facts: {
303
+ canDetectMention,
304
+ wasMentioned,
305
+ hasAnyMention: explicitMention.hasAnyMention,
306
+ implicitMentionKinds: implicitMentionKindWhen("quoted_bot", message.implicitMention === true)
307
+ },
308
+ policy: {
309
+ isGroup,
310
+ requireMention,
311
+ allowTextCommands: core.channel.commands.shouldHandleTextCommands({
312
+ cfg: config,
313
+ surface: "zalouser"
314
+ }),
315
+ hasControlCommand,
316
+ commandAuthorized: commandAuthorized === true
317
+ }
318
+ });
319
+ if (isGroup && requireMention && !canDetectMention && !mentionDecision.effectiveWasMentioned) {
320
+ runtime.error?.(`[${account.accountId}] zalouser mention required but detection unavailable (missing mention regexes and bot self id); dropping group ${chatId}`);
321
+ return;
322
+ }
323
+ if (isGroup && mentionDecision.shouldSkip) {
324
+ recordPendingHistoryEntryIfEnabled({
325
+ historyMap: historyState.groupHistories,
326
+ historyKey: historyKey ?? "",
327
+ limit: historyState.historyLimit,
328
+ entry: historyKey && rawBody ? {
329
+ sender: senderName || senderId,
330
+ body: rawBody,
331
+ timestamp: message.timestampMs,
332
+ messageId: resolveZalouserMessageSid({
333
+ msgId: message.msgId,
334
+ cliMsgId: message.cliMsgId,
335
+ fallback: `${message.timestampMs}`
336
+ })
337
+ } : null
338
+ });
339
+ logVerbose(core, runtime, `zalouser: skip group ${chatId} (mention required, not mentioned)`);
340
+ return;
341
+ }
342
+ const fromLabel = isGroup ? groupName || `group:${chatId}` : senderName || `user:${senderId}`;
343
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
344
+ const inboundSessionKey = resolveZalouserInboundSessionKey({
345
+ core,
346
+ config,
347
+ route,
348
+ storePath,
349
+ isGroup,
350
+ senderId
351
+ });
352
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
353
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
354
+ storePath,
355
+ sessionKey: inboundSessionKey
356
+ });
357
+ const body = core.channel.reply.formatAgentEnvelope({
358
+ channel: "Zalo Personal",
359
+ from: fromLabel,
360
+ timestamp: message.timestampMs,
361
+ previousTimestamp,
362
+ envelope: envelopeOptions,
363
+ body: rawBody
364
+ });
365
+ const combinedBody = isGroup && historyKey ? buildPendingHistoryContextFromMap({
366
+ historyMap: historyState.groupHistories,
367
+ historyKey,
368
+ limit: historyState.historyLimit,
369
+ currentMessage: body,
370
+ formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
371
+ channel: "Zalo Personal",
372
+ from: fromLabel,
373
+ timestamp: entry.timestamp,
374
+ envelope: envelopeOptions,
375
+ body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
376
+ })
377
+ }) : body;
378
+ const inboundHistory = isGroup && historyKey && historyState.historyLimit > 0 ? (historyState.groupHistories.get(historyKey) ?? []).map((entry) => ({
379
+ sender: entry.sender,
380
+ body: entry.body,
381
+ timestamp: entry.timestamp
382
+ })) : void 0;
383
+ const normalizedTo = isGroup ? `zalouser:group:${chatId}` : `zalouser:${chatId}`;
384
+ const messageSid = resolveZalouserMessageSid({
385
+ msgId: message.msgId,
386
+ cliMsgId: message.cliMsgId,
387
+ fallback: `${message.timestampMs}`
388
+ });
389
+ const messageSidFull = formatZalouserMessageSidFull({
390
+ msgId: message.msgId,
391
+ cliMsgId: message.cliMsgId
392
+ });
393
+ const ctxPayload = core.channel.turn.buildContext({
394
+ channel: "zalouser",
395
+ accountId: route.accountId,
396
+ messageId: messageSid,
397
+ messageIdFull: messageSidFull,
398
+ timestamp: message.timestampMs,
399
+ from: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
400
+ sender: {
401
+ id: senderId,
402
+ name: senderName || void 0
403
+ },
404
+ conversation: {
405
+ kind: isGroup ? "group" : "direct",
406
+ id: chatId,
407
+ label: fromLabel,
408
+ routePeer: {
409
+ kind: isGroup ? "group" : "direct",
410
+ id: chatId
411
+ }
412
+ },
413
+ route: {
414
+ agentId: route.agentId,
415
+ accountId: route.accountId,
416
+ routeSessionKey: route.sessionKey,
417
+ dispatchSessionKey: inboundSessionKey
418
+ },
419
+ reply: {
420
+ to: normalizedTo,
421
+ originatingTo: normalizedTo
422
+ },
423
+ message: {
424
+ body: combinedBody,
425
+ bodyForAgent: rawBody,
426
+ rawBody,
427
+ commandBody,
428
+ inboundHistory,
429
+ envelopeFrom: fromLabel
430
+ },
431
+ extra: {
432
+ BodyForCommands: commandBody,
433
+ GroupSubject: isGroup ? groupName || void 0 : void 0,
434
+ GroupChannel: isGroup ? groupName || void 0 : void 0,
435
+ GroupMembers: isGroup ? groupMembers : void 0,
436
+ WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : void 0,
437
+ CommandAuthorized: commandAuthorized
438
+ }
439
+ });
440
+ const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
441
+ cfg: config,
442
+ agentId: route.agentId,
443
+ channel: "zalouser",
444
+ accountId: account.accountId,
445
+ typing: {
446
+ start: async () => {
447
+ await sendTypingZalouser(chatId, {
448
+ profile: account.profile,
449
+ isGroup
450
+ });
451
+ },
452
+ onStartError: (err) => {
453
+ runtime.error?.(`[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`);
454
+ logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
455
+ }
456
+ }
457
+ });
458
+ await core.channel.turn.run({
459
+ channel: "zalouser",
460
+ accountId: account.accountId,
461
+ raw: message,
462
+ adapter: {
463
+ ingest: () => ({
464
+ id: messageSid ?? `${message.timestampMs}`,
465
+ timestamp: message.timestampMs,
466
+ rawText: rawBody,
467
+ textForAgent: rawBody,
468
+ textForCommands: commandBody,
469
+ raw: message
470
+ }),
471
+ resolveTurn: () => ({
472
+ cfg: config,
473
+ channel: "zalouser",
474
+ accountId: account.accountId,
475
+ agentId: route.agentId,
476
+ routeSessionKey: route.sessionKey,
477
+ storePath,
478
+ ctxPayload,
479
+ recordInboundSession: core.channel.session.recordInboundSession,
480
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
481
+ delivery: {
482
+ deliver: async (payload) => {
483
+ await deliverZalouserReply({
484
+ payload,
485
+ profile: account.profile,
486
+ chatId,
487
+ isGroup,
488
+ runtime,
489
+ core,
490
+ config,
491
+ accountId: account.accountId,
492
+ statusSink,
493
+ tableMode: core.channel.text.resolveMarkdownTableMode({
494
+ cfg: config,
495
+ channel: "zalouser",
496
+ accountId: account.accountId
497
+ })
498
+ });
499
+ },
500
+ onError: (err, info) => {
501
+ runtime.error(`[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`);
502
+ }
503
+ },
504
+ dispatcherOptions: replyPipeline,
505
+ replyOptions: { onModelSelected },
506
+ record: { onRecordError: (err) => {
507
+ runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
508
+ } }
509
+ })
510
+ }
511
+ });
512
+ if (isGroup && historyKey) clearHistoryEntriesIfEnabled({
513
+ historyMap: historyState.groupHistories,
514
+ historyKey,
515
+ limit: historyState.historyLimit
516
+ });
517
+ }
518
+ async function deliverZalouserReply(params) {
519
+ const { payload, profile, chatId, isGroup, runtime, core, config, accountId, statusSink } = params;
520
+ const tableMode = params.tableMode ?? "code";
521
+ const reply = resolveSendableOutboundReplyParts(payload, { text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode) });
522
+ const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
523
+ const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, { fallbackLimit: ZALOUSER_TEXT_LIMIT });
524
+ await deliverTextOrMediaReply({
525
+ payload,
526
+ text: reply.text,
527
+ sendText: async (chunk) => {
528
+ try {
529
+ await sendMessageZalouser(chatId, chunk, {
530
+ profile,
531
+ isGroup,
532
+ textMode: "markdown",
533
+ textChunkMode: chunkMode,
534
+ textChunkLimit
535
+ });
536
+ statusSink?.({ lastOutboundAt: Date.now() });
537
+ } catch (err) {
538
+ runtime.error(`Zalouser message send failed: ${String(err)}`);
539
+ }
540
+ },
541
+ sendMedia: async ({ mediaUrl, caption }) => {
542
+ logVerbose(core, runtime, `Sending media to ${chatId}`);
543
+ await sendMessageZalouser(chatId, caption ?? "", {
544
+ profile,
545
+ mediaUrl,
546
+ isGroup,
547
+ textMode: "markdown",
548
+ textChunkMode: chunkMode,
549
+ textChunkLimit
550
+ });
551
+ statusSink?.({ lastOutboundAt: Date.now() });
552
+ },
553
+ onMediaError: (error) => {
554
+ runtime.error(`Zalouser media send failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
555
+ }
556
+ });
557
+ }
558
+ async function monitorZalouserProvider(options) {
559
+ let { account, config } = options;
560
+ const { abortSignal, statusSink, runtime } = options;
561
+ const core = getZalouserRuntime();
562
+ const inboundQueue = new KeyedAsyncQueue();
563
+ const historyLimit = Math.max(0, account.config.historyLimit ?? config.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
564
+ const groupHistories = /* @__PURE__ */ new Map();
565
+ try {
566
+ const profile = account.profile;
567
+ const allowFromEntries = (account.config.allowFrom ?? []).map((entry) => normalizeZalouserEntry(String(entry))).filter((entry) => entry && entry !== "*");
568
+ const groupAllowFromEntries = (account.config.groupAllowFrom ?? []).map((entry) => normalizeZalouserEntry(String(entry))).filter((entry) => entry && entry !== "*");
569
+ if (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0) {
570
+ const byName = buildNameIndex(await listZaloFriends(profile), (friend) => friend.displayName);
571
+ if (allowFromEntries.length > 0) {
572
+ const { additions, mapping, unresolved } = resolveUserAllowlistEntries(allowFromEntries, byName);
573
+ const allowFrom = mergeAllowlist({
574
+ existing: account.config.allowFrom,
575
+ additions
576
+ });
577
+ account = {
578
+ ...account,
579
+ config: {
580
+ ...account.config,
581
+ allowFrom
582
+ }
583
+ };
584
+ summarizeMapping("zalouser users", mapping, unresolved, runtime);
585
+ }
586
+ if (groupAllowFromEntries.length > 0) {
587
+ const { additions, mapping, unresolved } = resolveUserAllowlistEntries(groupAllowFromEntries, byName);
588
+ const groupAllowFrom = mergeAllowlist({
589
+ existing: account.config.groupAllowFrom,
590
+ additions
591
+ });
592
+ account = {
593
+ ...account,
594
+ config: {
595
+ ...account.config,
596
+ groupAllowFrom
597
+ }
598
+ };
599
+ summarizeMapping("zalouser group users", mapping, unresolved, runtime);
600
+ }
601
+ }
602
+ const groupsConfig = account.config.groups ?? {};
603
+ const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
604
+ if (groupKeys.length > 0) {
605
+ const byName = buildNameIndex(await listZaloGroups(profile), (group) => group.name);
606
+ const mapping = [];
607
+ const unresolved = [];
608
+ const nextGroups = { ...groupsConfig };
609
+ for (const entry of groupKeys) {
610
+ const cleaned = normalizeZalouserEntry(entry);
611
+ if (/^\d+$/.test(cleaned)) {
612
+ if (!nextGroups[cleaned]) nextGroups[cleaned] = groupsConfig[entry];
613
+ mapping.push(`${entry}→${cleaned}`);
614
+ continue;
615
+ }
616
+ const id = (byName.get(normalizeLowercaseStringOrEmpty(cleaned)) ?? [])[0]?.groupId;
617
+ if (id) {
618
+ if (!nextGroups[id]) nextGroups[id] = groupsConfig[entry];
619
+ mapping.push(`${entry}→${id}`);
620
+ } else unresolved.push(entry);
621
+ }
622
+ account = {
623
+ ...account,
624
+ config: {
625
+ ...account.config,
626
+ groups: nextGroups
627
+ }
628
+ };
629
+ summarizeMapping("zalouser groups", mapping, unresolved, runtime);
630
+ }
631
+ } catch (err) {
632
+ runtime.log?.(`zalouser resolve failed; using config entries. ${String(err)}`);
633
+ }
634
+ let listenerStop = null;
635
+ let stopped = false;
636
+ const stop = () => {
637
+ if (stopped) return;
638
+ stopped = true;
639
+ listenerStop?.();
640
+ listenerStop = null;
641
+ };
642
+ let settled = false;
643
+ const { promise: waitForExit, resolve: resolveRun, reject: rejectRun } = createDeferred();
644
+ const settleSuccess = () => {
645
+ if (settled) return;
646
+ settled = true;
647
+ stop();
648
+ resolveRun();
649
+ };
650
+ const settleFailure = (error) => {
651
+ if (settled) return;
652
+ settled = true;
653
+ stop();
654
+ rejectRun(error instanceof Error ? error : new Error(String(error)));
655
+ };
656
+ const onAbort = () => {
657
+ settleSuccess();
658
+ };
659
+ abortSignal.addEventListener("abort", onAbort, { once: true });
660
+ let listener;
661
+ try {
662
+ listener = await startZaloListener({
663
+ accountId: account.accountId,
664
+ profile: account.profile,
665
+ abortSignal,
666
+ onMessage: (msg) => {
667
+ if (stopped) return;
668
+ logVerbose(core, runtime, `[${account.accountId}] inbound message`);
669
+ statusSink?.({ lastInboundAt: Date.now() });
670
+ const queueKey = resolveInboundQueueKey(msg);
671
+ inboundQueue.enqueue(queueKey, async () => {
672
+ if (stopped || abortSignal.aborted) return;
673
+ await processMessage(msg, account, config, core, runtime, {
674
+ historyLimit,
675
+ groupHistories
676
+ }, statusSink);
677
+ }).catch((err) => {
678
+ runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
679
+ });
680
+ },
681
+ onError: (err) => {
682
+ if (stopped || abortSignal.aborted) return;
683
+ runtime.error(`[${account.accountId}] Zalo listener error: ${String(err)}`);
684
+ settleFailure(err);
685
+ }
686
+ });
687
+ } catch (error) {
688
+ abortSignal.removeEventListener("abort", onAbort);
689
+ throw error;
690
+ }
691
+ listenerStop = listener.stop;
692
+ if (stopped) {
693
+ listenerStop();
694
+ listenerStop = null;
695
+ }
696
+ if (abortSignal.aborted) settleSuccess();
697
+ try {
698
+ await waitForExit;
699
+ } finally {
700
+ abortSignal.removeEventListener("abort", onAbort);
701
+ }
702
+ return { stop };
703
+ }
704
+ //#endregion
705
+ export { monitorZalouserProvider };