@openclaw/zalouser 2026.3.7 → 2026.3.10
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 +24 -0
- package/package.json +6 -1
- package/src/channel.directory.test.ts +72 -0
- package/src/channel.sendpayload.test.ts +72 -54
- package/src/channel.ts +122 -14
- package/src/config-schema.ts +12 -9
- package/src/monitor.group-gating.test.ts +379 -11
- package/src/monitor.ts +412 -114
- package/src/onboarding.ts +8 -31
- package/src/runtime.ts +4 -12
- package/src/types.ts +3 -0
- package/src/zalo-js.ts +269 -22
- package/src/zca-client.ts +45 -1
package/src/monitor.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DM_GROUP_ACCESS_REASON,
|
|
3
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
4
|
+
type HistoryEntry,
|
|
5
|
+
KeyedAsyncQueue,
|
|
6
|
+
buildPendingHistoryContextFromMap,
|
|
7
|
+
clearHistoryEntriesIfEnabled,
|
|
8
|
+
recordPendingHistoryEntryIfEnabled,
|
|
9
|
+
resolveDmGroupAccessWithLists,
|
|
10
|
+
} from "openclaw/plugin-sdk/compat";
|
|
1
11
|
import type {
|
|
2
12
|
MarkdownTableMode,
|
|
3
13
|
OpenClawConfig,
|
|
@@ -73,8 +83,111 @@ function buildNameIndex<T>(items: T[], nameFn: (item: T) => string | undefined):
|
|
|
73
83
|
return index;
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
function resolveUserAllowlistEntries(
|
|
87
|
+
entries: string[],
|
|
88
|
+
byName: Map<string, Array<{ userId: string }>>,
|
|
89
|
+
): {
|
|
90
|
+
additions: string[];
|
|
91
|
+
mapping: string[];
|
|
92
|
+
unresolved: string[];
|
|
93
|
+
} {
|
|
94
|
+
const additions: string[] = [];
|
|
95
|
+
const mapping: string[] = [];
|
|
96
|
+
const unresolved: string[] = [];
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (/^\d+$/.test(entry)) {
|
|
99
|
+
additions.push(entry);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const matches = byName.get(entry.toLowerCase()) ?? [];
|
|
103
|
+
const match = matches[0];
|
|
104
|
+
const id = match?.userId ? String(match.userId) : undefined;
|
|
105
|
+
if (id) {
|
|
106
|
+
additions.push(id);
|
|
107
|
+
mapping.push(`${entry}->${id}`);
|
|
108
|
+
} else {
|
|
109
|
+
unresolved.push(entry);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { additions, mapping, unresolved };
|
|
113
|
+
}
|
|
114
|
+
|
|
76
115
|
type ZalouserCoreRuntime = ReturnType<typeof getZalouserRuntime>;
|
|
77
116
|
|
|
117
|
+
type ZalouserGroupHistoryState = {
|
|
118
|
+
historyLimit: number;
|
|
119
|
+
groupHistories: Map<string, HistoryEntry[]>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function resolveInboundQueueKey(message: ZaloInboundMessage): string {
|
|
123
|
+
const threadId = message.threadId?.trim() || "unknown";
|
|
124
|
+
if (message.isGroup) {
|
|
125
|
+
return `group:${threadId}`;
|
|
126
|
+
}
|
|
127
|
+
const senderId = message.senderId?.trim();
|
|
128
|
+
return `direct:${senderId || threadId}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createDeferred<T>() {
|
|
132
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
133
|
+
let reject!: (reason?: unknown) => void;
|
|
134
|
+
const promise = new Promise<T>((res, rej) => {
|
|
135
|
+
resolve = res;
|
|
136
|
+
reject = rej;
|
|
137
|
+
});
|
|
138
|
+
return { promise, resolve, reject };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveZalouserDmSessionScope(config: OpenClawConfig) {
|
|
142
|
+
const configured = config.session?.dmScope;
|
|
143
|
+
return configured === "main" || !configured ? "per-channel-peer" : configured;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveZalouserInboundSessionKey(params: {
|
|
147
|
+
core: ZalouserCoreRuntime;
|
|
148
|
+
config: OpenClawConfig;
|
|
149
|
+
route: { agentId: string; accountId: string; sessionKey: string };
|
|
150
|
+
storePath: string;
|
|
151
|
+
isGroup: boolean;
|
|
152
|
+
senderId: string;
|
|
153
|
+
}): string {
|
|
154
|
+
if (params.isGroup) {
|
|
155
|
+
return params.route.sessionKey;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const directSessionKey = params.core.channel.routing
|
|
159
|
+
.buildAgentSessionKey({
|
|
160
|
+
agentId: params.route.agentId,
|
|
161
|
+
channel: "zalouser",
|
|
162
|
+
accountId: params.route.accountId,
|
|
163
|
+
peer: { kind: "direct", id: params.senderId },
|
|
164
|
+
dmScope: resolveZalouserDmSessionScope(params.config),
|
|
165
|
+
identityLinks: params.config.session?.identityLinks,
|
|
166
|
+
})
|
|
167
|
+
.toLowerCase();
|
|
168
|
+
const legacySessionKey = params.core.channel.routing
|
|
169
|
+
.buildAgentSessionKey({
|
|
170
|
+
agentId: params.route.agentId,
|
|
171
|
+
channel: "zalouser",
|
|
172
|
+
accountId: params.route.accountId,
|
|
173
|
+
peer: { kind: "group", id: params.senderId },
|
|
174
|
+
})
|
|
175
|
+
.toLowerCase();
|
|
176
|
+
const hasDirectSession =
|
|
177
|
+
params.core.channel.session.readSessionUpdatedAt({
|
|
178
|
+
storePath: params.storePath,
|
|
179
|
+
sessionKey: directSessionKey,
|
|
180
|
+
}) !== undefined;
|
|
181
|
+
const hasLegacySession =
|
|
182
|
+
params.core.channel.session.readSessionUpdatedAt({
|
|
183
|
+
storePath: params.storePath,
|
|
184
|
+
sessionKey: legacySessionKey,
|
|
185
|
+
}) !== undefined;
|
|
186
|
+
|
|
187
|
+
// Keep existing DM history on upgrade, but use canonical direct keys for new sessions.
|
|
188
|
+
return hasLegacySession && !hasDirectSession ? legacySessionKey : directSessionKey;
|
|
189
|
+
}
|
|
190
|
+
|
|
78
191
|
function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: string): void {
|
|
79
192
|
if (core.logging.shouldLogVerbose()) {
|
|
80
193
|
runtime.log(`[zalouser] ${message}`);
|
|
@@ -139,6 +252,7 @@ async function processMessage(
|
|
|
139
252
|
config: OpenClawConfig,
|
|
140
253
|
core: ZalouserCoreRuntime,
|
|
141
254
|
runtime: RuntimeEnv,
|
|
255
|
+
historyState: ZalouserGroupHistoryState,
|
|
142
256
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
143
257
|
): Promise<void> {
|
|
144
258
|
const pairing = createScopedPairingAccess({
|
|
@@ -151,6 +265,7 @@ async function processMessage(
|
|
|
151
265
|
if (!rawBody) {
|
|
152
266
|
return;
|
|
153
267
|
}
|
|
268
|
+
const commandBody = message.commandContent?.trim() || rawBody;
|
|
154
269
|
|
|
155
270
|
const isGroup = message.isGroup;
|
|
156
271
|
const chatId = message.threadId;
|
|
@@ -237,65 +352,90 @@ async function processMessage(
|
|
|
237
352
|
|
|
238
353
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
239
354
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
355
|
+
const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((v) => String(v));
|
|
356
|
+
const shouldComputeCommandAuth = core.channel.commands.shouldComputeCommandAuthorized(
|
|
357
|
+
commandBody,
|
|
358
|
+
config,
|
|
359
|
+
);
|
|
360
|
+
const storeAllowFrom =
|
|
361
|
+
!isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeCommandAuth)
|
|
362
|
+
? await pairing.readAllowFromStore().catch(() => [])
|
|
363
|
+
: [];
|
|
364
|
+
const accessDecision = resolveDmGroupAccessWithLists({
|
|
243
365
|
isGroup,
|
|
244
366
|
dmPolicy,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
|
251
|
-
resolveCommandAuthorizedFromAuthorizers: (params) =>
|
|
252
|
-
core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
|
|
367
|
+
groupPolicy,
|
|
368
|
+
allowFrom: configAllowFrom,
|
|
369
|
+
groupAllowFrom: configGroupAllowFrom,
|
|
370
|
+
storeAllowFrom,
|
|
371
|
+
isSenderAllowed: (allowFrom) => isSenderAllowed(senderId, allowFrom),
|
|
253
372
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
373
|
+
if (isGroup && accessDecision.decision !== "allow") {
|
|
374
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST) {
|
|
375
|
+
logVerbose(core, runtime, "Blocked zalouser group message (no group allowlist)");
|
|
376
|
+
} else if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED) {
|
|
377
|
+
logVerbose(
|
|
378
|
+
core,
|
|
379
|
+
runtime,
|
|
380
|
+
`Blocked zalouser sender ${senderId} (not in groupAllowFrom/allowFrom)`,
|
|
381
|
+
);
|
|
259
382
|
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
260
385
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
},
|
|
278
|
-
onReplyError: (err) => {
|
|
279
|
-
logVerbose(
|
|
280
|
-
core,
|
|
281
|
-
runtime,
|
|
282
|
-
`zalouser pairing reply failed for ${senderId}: ${String(err)}`,
|
|
283
|
-
);
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
} else {
|
|
386
|
+
if (!isGroup && accessDecision.decision !== "allow") {
|
|
387
|
+
if (accessDecision.decision === "pairing") {
|
|
388
|
+
await issuePairingChallenge({
|
|
389
|
+
channel: "zalouser",
|
|
390
|
+
senderId,
|
|
391
|
+
senderIdLine: `Your Zalo user id: ${senderId}`,
|
|
392
|
+
meta: { name: senderName || undefined },
|
|
393
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
394
|
+
onCreated: () => {
|
|
395
|
+
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
|
|
396
|
+
},
|
|
397
|
+
sendPairingReply: async (text) => {
|
|
398
|
+
await sendMessageZalouser(chatId, text, { profile: account.profile });
|
|
399
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
400
|
+
},
|
|
401
|
+
onReplyError: (err) => {
|
|
287
402
|
logVerbose(
|
|
288
403
|
core,
|
|
289
404
|
runtime,
|
|
290
|
-
`
|
|
405
|
+
`zalouser pairing reply failed for ${senderId}: ${String(err)}`,
|
|
291
406
|
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (accessDecision.reasonCode === DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED) {
|
|
412
|
+
logVerbose(core, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
|
|
413
|
+
} else {
|
|
414
|
+
logVerbose(
|
|
415
|
+
core,
|
|
416
|
+
runtime,
|
|
417
|
+
`Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`,
|
|
418
|
+
);
|
|
295
419
|
}
|
|
420
|
+
return;
|
|
296
421
|
}
|
|
297
422
|
|
|
298
|
-
const
|
|
423
|
+
const { commandAuthorized } = await resolveSenderCommandAuthorization({
|
|
424
|
+
cfg: config,
|
|
425
|
+
rawBody: commandBody,
|
|
426
|
+
isGroup,
|
|
427
|
+
dmPolicy,
|
|
428
|
+
configuredAllowFrom: configAllowFrom,
|
|
429
|
+
configuredGroupAllowFrom: configGroupAllowFrom,
|
|
430
|
+
senderId,
|
|
431
|
+
isSenderAllowed,
|
|
432
|
+
readAllowFromStore: async () => storeAllowFrom,
|
|
433
|
+
shouldComputeCommandAuthorized: (body, cfg) =>
|
|
434
|
+
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
|
435
|
+
resolveCommandAuthorizedFromAuthorizers: (params) =>
|
|
436
|
+
core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params),
|
|
437
|
+
});
|
|
438
|
+
const hasControlCommand = core.channel.commands.isControlCommandMessage(commandBody, config);
|
|
299
439
|
if (isGroup && hasControlCommand && commandAuthorized !== true) {
|
|
300
440
|
logVerbose(
|
|
301
441
|
core,
|
|
@@ -307,18 +447,19 @@ async function processMessage(
|
|
|
307
447
|
|
|
308
448
|
const peer = isGroup
|
|
309
449
|
? { kind: "group" as const, id: chatId }
|
|
310
|
-
: { kind: "
|
|
450
|
+
: { kind: "direct" as const, id: senderId };
|
|
311
451
|
|
|
312
452
|
const route = core.channel.routing.resolveAgentRoute({
|
|
313
453
|
cfg: config,
|
|
314
454
|
channel: "zalouser",
|
|
315
455
|
accountId: account.accountId,
|
|
316
456
|
peer: {
|
|
317
|
-
//
|
|
457
|
+
// Keep DM peer kind as "direct" so session keys follow dmScope and UI labels stay DM-shaped.
|
|
318
458
|
kind: peer.kind,
|
|
319
459
|
id: peer.id,
|
|
320
460
|
},
|
|
321
461
|
});
|
|
462
|
+
const historyKey = isGroup ? route.sessionKey : undefined;
|
|
322
463
|
|
|
323
464
|
const requireMention = isGroup
|
|
324
465
|
? resolveGroupRequireMention({
|
|
@@ -340,10 +481,11 @@ async function processMessage(
|
|
|
340
481
|
explicit: explicitMention,
|
|
341
482
|
})
|
|
342
483
|
: true;
|
|
484
|
+
const canDetectMention = mentionRegexes.length > 0 || explicitMention.canResolveExplicit;
|
|
343
485
|
const mentionGate = resolveMentionGatingWithBypass({
|
|
344
486
|
isGroup,
|
|
345
487
|
requireMention,
|
|
346
|
-
canDetectMention
|
|
488
|
+
canDetectMention,
|
|
347
489
|
wasMentioned,
|
|
348
490
|
implicitMention: message.implicitMention === true,
|
|
349
491
|
hasAnyMention: explicitMention.hasAnyMention,
|
|
@@ -354,7 +496,32 @@ async function processMessage(
|
|
|
354
496
|
hasControlCommand,
|
|
355
497
|
commandAuthorized: commandAuthorized === true,
|
|
356
498
|
});
|
|
499
|
+
if (isGroup && requireMention && !canDetectMention && !mentionGate.effectiveWasMentioned) {
|
|
500
|
+
runtime.error?.(
|
|
501
|
+
`[${account.accountId}] zalouser mention required but detection unavailable ` +
|
|
502
|
+
`(missing mention regexes and bot self id); dropping group ${chatId}`,
|
|
503
|
+
);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
357
506
|
if (isGroup && mentionGate.shouldSkip) {
|
|
507
|
+
recordPendingHistoryEntryIfEnabled({
|
|
508
|
+
historyMap: historyState.groupHistories,
|
|
509
|
+
historyKey: historyKey ?? "",
|
|
510
|
+
limit: historyState.historyLimit,
|
|
511
|
+
entry:
|
|
512
|
+
historyKey && rawBody
|
|
513
|
+
? {
|
|
514
|
+
sender: senderName || senderId,
|
|
515
|
+
body: rawBody,
|
|
516
|
+
timestamp: message.timestampMs,
|
|
517
|
+
messageId: resolveZalouserMessageSid({
|
|
518
|
+
msgId: message.msgId,
|
|
519
|
+
cliMsgId: message.cliMsgId,
|
|
520
|
+
fallback: `${message.timestampMs}`,
|
|
521
|
+
}),
|
|
522
|
+
}
|
|
523
|
+
: null,
|
|
524
|
+
});
|
|
358
525
|
logVerbose(core, runtime, `zalouser: skip group ${chatId} (mention required, not mentioned)`);
|
|
359
526
|
return;
|
|
360
527
|
}
|
|
@@ -363,10 +530,18 @@ async function processMessage(
|
|
|
363
530
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
364
531
|
agentId: route.agentId,
|
|
365
532
|
});
|
|
533
|
+
const inboundSessionKey = resolveZalouserInboundSessionKey({
|
|
534
|
+
core,
|
|
535
|
+
config,
|
|
536
|
+
route,
|
|
537
|
+
storePath,
|
|
538
|
+
isGroup,
|
|
539
|
+
senderId,
|
|
540
|
+
});
|
|
366
541
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
|
|
367
542
|
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
368
543
|
storePath,
|
|
369
|
-
sessionKey:
|
|
544
|
+
sessionKey: inboundSessionKey,
|
|
370
545
|
});
|
|
371
546
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
372
547
|
channel: "Zalo Personal",
|
|
@@ -376,15 +551,46 @@ async function processMessage(
|
|
|
376
551
|
envelope: envelopeOptions,
|
|
377
552
|
body: rawBody,
|
|
378
553
|
});
|
|
554
|
+
const combinedBody =
|
|
555
|
+
isGroup && historyKey
|
|
556
|
+
? buildPendingHistoryContextFromMap({
|
|
557
|
+
historyMap: historyState.groupHistories,
|
|
558
|
+
historyKey,
|
|
559
|
+
limit: historyState.historyLimit,
|
|
560
|
+
currentMessage: body,
|
|
561
|
+
formatEntry: (entry) =>
|
|
562
|
+
core.channel.reply.formatAgentEnvelope({
|
|
563
|
+
channel: "Zalo Personal",
|
|
564
|
+
from: fromLabel,
|
|
565
|
+
timestamp: entry.timestamp,
|
|
566
|
+
envelope: envelopeOptions,
|
|
567
|
+
body: `${entry.sender}: ${entry.body}${
|
|
568
|
+
entry.messageId ? ` [id:${entry.messageId}]` : ""
|
|
569
|
+
}`,
|
|
570
|
+
}),
|
|
571
|
+
})
|
|
572
|
+
: body;
|
|
573
|
+
const inboundHistory =
|
|
574
|
+
isGroup && historyKey && historyState.historyLimit > 0
|
|
575
|
+
? (historyState.groupHistories.get(historyKey) ?? []).map((entry) => ({
|
|
576
|
+
sender: entry.sender,
|
|
577
|
+
body: entry.body,
|
|
578
|
+
timestamp: entry.timestamp,
|
|
579
|
+
}))
|
|
580
|
+
: undefined;
|
|
581
|
+
|
|
582
|
+
const normalizedTo = isGroup ? `zalouser:group:${chatId}` : `zalouser:${chatId}`;
|
|
379
583
|
|
|
380
584
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
381
|
-
Body:
|
|
585
|
+
Body: combinedBody,
|
|
382
586
|
BodyForAgent: rawBody,
|
|
587
|
+
InboundHistory: inboundHistory,
|
|
383
588
|
RawBody: rawBody,
|
|
384
|
-
CommandBody:
|
|
589
|
+
CommandBody: commandBody,
|
|
590
|
+
BodyForCommands: commandBody,
|
|
385
591
|
From: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
|
|
386
|
-
To:
|
|
387
|
-
SessionKey:
|
|
592
|
+
To: normalizedTo,
|
|
593
|
+
SessionKey: inboundSessionKey,
|
|
388
594
|
AccountId: route.accountId,
|
|
389
595
|
ChatType: isGroup ? "group" : "direct",
|
|
390
596
|
ConversationLabel: fromLabel,
|
|
@@ -407,7 +613,7 @@ async function processMessage(
|
|
|
407
613
|
cliMsgId: message.cliMsgId,
|
|
408
614
|
}),
|
|
409
615
|
OriginatingChannel: "zalouser",
|
|
410
|
-
OriginatingTo:
|
|
616
|
+
OriginatingTo: normalizedTo,
|
|
411
617
|
});
|
|
412
618
|
|
|
413
619
|
await core.channel.session.recordInboundSession({
|
|
@@ -433,6 +639,9 @@ async function processMessage(
|
|
|
433
639
|
});
|
|
434
640
|
},
|
|
435
641
|
onStartError: (err) => {
|
|
642
|
+
runtime.error?.(
|
|
643
|
+
`[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`,
|
|
644
|
+
);
|
|
436
645
|
logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
|
|
437
646
|
},
|
|
438
647
|
});
|
|
@@ -469,6 +678,13 @@ async function processMessage(
|
|
|
469
678
|
onModelSelected,
|
|
470
679
|
},
|
|
471
680
|
});
|
|
681
|
+
if (isGroup && historyKey) {
|
|
682
|
+
clearHistoryEntriesIfEnabled({
|
|
683
|
+
historyMap: historyState.groupHistories,
|
|
684
|
+
historyKey,
|
|
685
|
+
limit: historyState.historyLimit,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
472
688
|
}
|
|
473
689
|
|
|
474
690
|
async function deliverZalouserReply(params: {
|
|
@@ -534,43 +750,60 @@ export async function monitorZalouserProvider(
|
|
|
534
750
|
const { abortSignal, statusSink, runtime } = options;
|
|
535
751
|
|
|
536
752
|
const core = getZalouserRuntime();
|
|
753
|
+
const inboundQueue = new KeyedAsyncQueue();
|
|
754
|
+
const historyLimit = Math.max(
|
|
755
|
+
0,
|
|
756
|
+
account.config.historyLimit ??
|
|
757
|
+
config.messages?.groupChat?.historyLimit ??
|
|
758
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
759
|
+
);
|
|
760
|
+
const groupHistories = new Map<string, HistoryEntry[]>();
|
|
537
761
|
|
|
538
762
|
try {
|
|
539
763
|
const profile = account.profile;
|
|
540
764
|
const allowFromEntries = (account.config.allowFrom ?? [])
|
|
541
765
|
.map((entry) => normalizeZalouserEntry(String(entry)))
|
|
542
766
|
.filter((entry) => entry && entry !== "*");
|
|
767
|
+
const groupAllowFromEntries = (account.config.groupAllowFrom ?? [])
|
|
768
|
+
.map((entry) => normalizeZalouserEntry(String(entry)))
|
|
769
|
+
.filter((entry) => entry && entry !== "*");
|
|
543
770
|
|
|
544
|
-
if (allowFromEntries.length > 0) {
|
|
771
|
+
if (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0) {
|
|
545
772
|
const friends = await listZaloFriends(profile);
|
|
546
773
|
const byName = buildNameIndex(friends, (friend) => friend.displayName);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
774
|
+
if (allowFromEntries.length > 0) {
|
|
775
|
+
const { additions, mapping, unresolved } = resolveUserAllowlistEntries(
|
|
776
|
+
allowFromEntries,
|
|
777
|
+
byName,
|
|
778
|
+
);
|
|
779
|
+
const allowFrom = mergeAllowlist({ existing: account.config.allowFrom, additions });
|
|
780
|
+
account = {
|
|
781
|
+
...account,
|
|
782
|
+
config: {
|
|
783
|
+
...account.config,
|
|
784
|
+
allowFrom,
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
summarizeMapping("zalouser users", mapping, unresolved, runtime);
|
|
788
|
+
}
|
|
789
|
+
if (groupAllowFromEntries.length > 0) {
|
|
790
|
+
const { additions, mapping, unresolved } = resolveUserAllowlistEntries(
|
|
791
|
+
groupAllowFromEntries,
|
|
792
|
+
byName,
|
|
793
|
+
);
|
|
794
|
+
const groupAllowFrom = mergeAllowlist({
|
|
795
|
+
existing: account.config.groupAllowFrom,
|
|
796
|
+
additions,
|
|
797
|
+
});
|
|
798
|
+
account = {
|
|
799
|
+
...account,
|
|
800
|
+
config: {
|
|
801
|
+
...account.config,
|
|
802
|
+
groupAllowFrom,
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
summarizeMapping("zalouser group users", mapping, unresolved, runtime);
|
|
564
806
|
}
|
|
565
|
-
const allowFrom = mergeAllowlist({ existing: account.config.allowFrom, additions });
|
|
566
|
-
account = {
|
|
567
|
-
...account,
|
|
568
|
-
config: {
|
|
569
|
-
...account.config,
|
|
570
|
-
allowFrom,
|
|
571
|
-
},
|
|
572
|
-
};
|
|
573
|
-
summarizeMapping("zalouser users", mapping, unresolved, runtime);
|
|
574
807
|
}
|
|
575
808
|
|
|
576
809
|
const groupsConfig = account.config.groups ?? {};
|
|
@@ -627,40 +860,92 @@ export async function monitorZalouserProvider(
|
|
|
627
860
|
listenerStop = null;
|
|
628
861
|
};
|
|
629
862
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
profile: account.profile,
|
|
633
|
-
abortSignal,
|
|
634
|
-
onMessage: (msg) => {
|
|
635
|
-
if (stopped) {
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
logVerbose(core, runtime, `[${account.accountId}] inbound message`);
|
|
639
|
-
statusSink?.({ lastInboundAt: Date.now() });
|
|
640
|
-
processMessage(msg, account, config, core, runtime, statusSink).catch((err) => {
|
|
641
|
-
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
|
642
|
-
});
|
|
643
|
-
},
|
|
644
|
-
onError: (err) => {
|
|
645
|
-
if (stopped || abortSignal.aborted) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
runtime.error(`[${account.accountId}] Zalo listener error: ${String(err)}`);
|
|
649
|
-
},
|
|
650
|
-
});
|
|
863
|
+
let settled = false;
|
|
864
|
+
const { promise: waitForExit, resolve: resolveRun, reject: rejectRun } = createDeferred<void>();
|
|
651
865
|
|
|
652
|
-
|
|
866
|
+
const settleSuccess = () => {
|
|
867
|
+
if (settled) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
settled = true;
|
|
871
|
+
stop();
|
|
872
|
+
resolveRun();
|
|
873
|
+
};
|
|
653
874
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
875
|
+
const settleFailure = (error: unknown) => {
|
|
876
|
+
if (settled) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
settled = true;
|
|
880
|
+
stop();
|
|
881
|
+
rejectRun(error instanceof Error ? error : new Error(String(error)));
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const onAbort = () => {
|
|
885
|
+
settleSuccess();
|
|
886
|
+
};
|
|
887
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
888
|
+
|
|
889
|
+
let listener: Awaited<ReturnType<typeof startZaloListener>>;
|
|
890
|
+
try {
|
|
891
|
+
listener = await startZaloListener({
|
|
892
|
+
accountId: account.accountId,
|
|
893
|
+
profile: account.profile,
|
|
894
|
+
abortSignal,
|
|
895
|
+
onMessage: (msg) => {
|
|
896
|
+
if (stopped) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
logVerbose(core, runtime, `[${account.accountId}] inbound message`);
|
|
900
|
+
statusSink?.({ lastInboundAt: Date.now() });
|
|
901
|
+
const queueKey = resolveInboundQueueKey(msg);
|
|
902
|
+
void inboundQueue
|
|
903
|
+
.enqueue(queueKey, async () => {
|
|
904
|
+
if (stopped || abortSignal.aborted) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
await processMessage(
|
|
908
|
+
msg,
|
|
909
|
+
account,
|
|
910
|
+
config,
|
|
911
|
+
core,
|
|
912
|
+
runtime,
|
|
913
|
+
{ historyLimit, groupHistories },
|
|
914
|
+
statusSink,
|
|
915
|
+
);
|
|
916
|
+
})
|
|
917
|
+
.catch((err) => {
|
|
918
|
+
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
|
919
|
+
});
|
|
660
920
|
},
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
921
|
+
onError: (err) => {
|
|
922
|
+
if (stopped || abortSignal.aborted) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
runtime.error(`[${account.accountId}] Zalo listener error: ${String(err)}`);
|
|
926
|
+
settleFailure(err);
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
} catch (error) {
|
|
930
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
listenerStop = listener.stop;
|
|
935
|
+
if (stopped) {
|
|
936
|
+
listenerStop();
|
|
937
|
+
listenerStop = null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (abortSignal.aborted) {
|
|
941
|
+
settleSuccess();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
await waitForExit;
|
|
946
|
+
} finally {
|
|
947
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
948
|
+
}
|
|
664
949
|
|
|
665
950
|
return { stop };
|
|
666
951
|
}
|
|
@@ -671,14 +956,27 @@ export const __testing = {
|
|
|
671
956
|
account: ResolvedZalouserAccount;
|
|
672
957
|
config: OpenClawConfig;
|
|
673
958
|
runtime: RuntimeEnv;
|
|
959
|
+
historyState?: {
|
|
960
|
+
historyLimit?: number;
|
|
961
|
+
groupHistories?: Map<string, HistoryEntry[]>;
|
|
962
|
+
};
|
|
674
963
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
675
964
|
}) => {
|
|
965
|
+
const historyLimit = Math.max(
|
|
966
|
+
0,
|
|
967
|
+
params.historyState?.historyLimit ??
|
|
968
|
+
params.account.config.historyLimit ??
|
|
969
|
+
params.config.messages?.groupChat?.historyLimit ??
|
|
970
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
971
|
+
);
|
|
972
|
+
const groupHistories = params.historyState?.groupHistories ?? new Map<string, HistoryEntry[]>();
|
|
676
973
|
await processMessage(
|
|
677
974
|
params.message,
|
|
678
975
|
params.account,
|
|
679
976
|
params.config,
|
|
680
977
|
getZalouserRuntime(),
|
|
681
978
|
params.runtime,
|
|
979
|
+
{ historyLimit, groupHistories },
|
|
682
980
|
params.statusSink,
|
|
683
981
|
);
|
|
684
982
|
},
|