@openclaw/zalouser 2026.3.1 → 2026.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +41 -147
- package/index.ts +1 -3
- package/package.json +4 -3
- package/src/accounts.test.ts +214 -0
- package/src/accounts.ts +17 -14
- package/src/channel.sendpayload.test.ts +117 -0
- package/src/channel.test.ts +123 -1
- package/src/channel.ts +244 -191
- package/src/config-schema.ts +1 -0
- package/src/group-policy.test.ts +49 -0
- package/src/group-policy.ts +78 -0
- package/src/message-sid.test.ts +66 -0
- package/src/message-sid.ts +80 -0
- package/src/monitor.account-scope.test.ts +123 -0
- package/src/monitor.group-gating.test.ts +216 -0
- package/src/monitor.ts +291 -225
- package/src/onboarding.ts +110 -142
- package/src/probe.test.ts +60 -0
- package/src/probe.ts +19 -12
- package/src/reaction.test.ts +19 -0
- package/src/reaction.ts +29 -0
- package/src/send.test.ts +116 -115
- package/src/send.ts +63 -117
- package/src/status-issues.test.ts +1 -15
- package/src/status-issues.ts +7 -26
- package/src/tool.test.ts +149 -0
- package/src/tool.ts +36 -54
- package/src/types.ts +52 -42
- package/src/zalo-js.ts +1401 -0
- package/src/zca-client.ts +249 -0
- package/src/zca-js-exports.d.ts +22 -0
- package/src/zca.ts +0 -198
package/src/monitor.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ChildProcess } from "node:child_process";
|
|
2
1
|
import type {
|
|
3
2
|
MarkdownTableMode,
|
|
4
3
|
OpenClawConfig,
|
|
@@ -6,10 +5,12 @@ import type {
|
|
|
6
5
|
RuntimeEnv,
|
|
7
6
|
} from "openclaw/plugin-sdk";
|
|
8
7
|
import {
|
|
8
|
+
createTypingCallbacks,
|
|
9
9
|
createScopedPairingAccess,
|
|
10
10
|
createReplyPrefixOptions,
|
|
11
11
|
resolveOutboundMediaUrls,
|
|
12
12
|
mergeAllowlist,
|
|
13
|
+
resolveMentionGatingWithBypass,
|
|
13
14
|
resolveOpenProviderRuntimeGroupPolicy,
|
|
14
15
|
resolveDefaultGroupPolicy,
|
|
15
16
|
resolveSenderCommandAuthorization,
|
|
@@ -17,10 +18,26 @@ import {
|
|
|
17
18
|
summarizeMapping,
|
|
18
19
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
19
20
|
} from "openclaw/plugin-sdk";
|
|
21
|
+
import {
|
|
22
|
+
buildZalouserGroupCandidates,
|
|
23
|
+
findZalouserGroupEntry,
|
|
24
|
+
isZalouserGroupEntryAllowed,
|
|
25
|
+
} from "./group-policy.js";
|
|
26
|
+
import { formatZalouserMessageSidFull, resolveZalouserMessageSid } from "./message-sid.js";
|
|
20
27
|
import { getZalouserRuntime } from "./runtime.js";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
import {
|
|
29
|
+
sendDeliveredZalouser,
|
|
30
|
+
sendMessageZalouser,
|
|
31
|
+
sendSeenZalouser,
|
|
32
|
+
sendTypingZalouser,
|
|
33
|
+
} from "./send.js";
|
|
34
|
+
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
|
|
35
|
+
import {
|
|
36
|
+
listZaloFriends,
|
|
37
|
+
listZaloGroups,
|
|
38
|
+
resolveZaloGroupContext,
|
|
39
|
+
startZaloListener,
|
|
40
|
+
} from "./zalo-js.js";
|
|
24
41
|
|
|
25
42
|
export type ZalouserMonitorOptions = {
|
|
26
43
|
account: ResolvedZalouserAccount;
|
|
@@ -62,136 +79,133 @@ function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: str
|
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
|
82
|
+
function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boolean {
|
|
66
83
|
if (allowFrom.includes("*")) {
|
|
67
84
|
return true;
|
|
68
85
|
}
|
|
69
|
-
const normalizedSenderId = senderId.toLowerCase();
|
|
86
|
+
const normalizedSenderId = senderId?.trim().toLowerCase();
|
|
87
|
+
if (!normalizedSenderId) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
70
90
|
return allowFrom.some((entry) => {
|
|
71
91
|
const normalized = entry.toLowerCase().replace(/^(zalouser|zlu):/i, "");
|
|
72
92
|
return normalized === normalizedSenderId;
|
|
73
93
|
});
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
function normalizeGroupSlug(raw?: string | null): string {
|
|
77
|
-
const trimmed = raw?.trim().toLowerCase() ?? "";
|
|
78
|
-
if (!trimmed) {
|
|
79
|
-
return "";
|
|
80
|
-
}
|
|
81
|
-
return trimmed
|
|
82
|
-
.replace(/^#/, "")
|
|
83
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
84
|
-
.replace(/^-+|-+$/g, "");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
96
|
function isGroupAllowed(params: {
|
|
88
97
|
groupId: string;
|
|
89
98
|
groupName?: string | null;
|
|
90
|
-
groups: Record<string, { allow?: boolean; enabled?: boolean }>;
|
|
99
|
+
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
|
91
100
|
}): boolean {
|
|
92
101
|
const groups = params.groups ?? {};
|
|
93
102
|
const keys = Object.keys(groups);
|
|
94
103
|
if (keys.length === 0) {
|
|
95
104
|
return false;
|
|
96
105
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
return entry.allow !== false && entry.enabled !== false;
|
|
109
|
-
}
|
|
110
|
-
const wildcard = groups["*"];
|
|
111
|
-
if (wildcard) {
|
|
112
|
-
return wildcard.allow !== false && wildcard.enabled !== false;
|
|
113
|
-
}
|
|
114
|
-
return false;
|
|
106
|
+
const entry = findZalouserGroupEntry(
|
|
107
|
+
groups,
|
|
108
|
+
buildZalouserGroupCandidates({
|
|
109
|
+
groupId: params.groupId,
|
|
110
|
+
groupName: params.groupName,
|
|
111
|
+
includeGroupIdAlias: true,
|
|
112
|
+
includeWildcard: true,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
return isZalouserGroupEntryAllowed(entry);
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const parsed = JSON.parse(trimmed) as ZcaMessage;
|
|
139
|
-
onMessage(parsed);
|
|
140
|
-
} catch {
|
|
141
|
-
// ignore non-JSON lines
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
onError,
|
|
146
|
-
});
|
|
118
|
+
function resolveGroupRequireMention(params: {
|
|
119
|
+
groupId: string;
|
|
120
|
+
groupName?: string | null;
|
|
121
|
+
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
|
122
|
+
}): boolean {
|
|
123
|
+
const entry = findZalouserGroupEntry(
|
|
124
|
+
params.groups ?? {},
|
|
125
|
+
buildZalouserGroupCandidates({
|
|
126
|
+
groupId: params.groupId,
|
|
127
|
+
groupName: params.groupName,
|
|
128
|
+
includeGroupIdAlias: true,
|
|
129
|
+
includeWildcard: true,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
if (typeof entry?.requireMention === "boolean") {
|
|
133
|
+
return entry.requireMention;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
138
|
+
async function sendZalouserDeliveryAcks(params: {
|
|
139
|
+
profile: string;
|
|
140
|
+
isGroup: boolean;
|
|
141
|
+
message: NonNullable<ZaloInboundMessage["eventMessage"]>;
|
|
142
|
+
}): Promise<void> {
|
|
143
|
+
await sendDeliveredZalouser({
|
|
144
|
+
profile: params.profile,
|
|
145
|
+
isGroup: params.isGroup,
|
|
146
|
+
message: params.message,
|
|
147
|
+
isSeen: true,
|
|
153
148
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
149
|
+
await sendSeenZalouser({
|
|
150
|
+
profile: params.profile,
|
|
151
|
+
isGroup: params.isGroup,
|
|
152
|
+
message: params.message,
|
|
159
153
|
});
|
|
160
|
-
|
|
161
|
-
abortSignal.addEventListener(
|
|
162
|
-
"abort",
|
|
163
|
-
() => {
|
|
164
|
-
proc.kill("SIGTERM");
|
|
165
|
-
},
|
|
166
|
-
{ once: true },
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return proc;
|
|
170
154
|
}
|
|
171
155
|
|
|
172
156
|
async function processMessage(
|
|
173
|
-
message:
|
|
157
|
+
message: ZaloInboundMessage,
|
|
174
158
|
account: ResolvedZalouserAccount,
|
|
175
159
|
config: OpenClawConfig,
|
|
176
160
|
core: ZalouserCoreRuntime,
|
|
177
161
|
runtime: RuntimeEnv,
|
|
178
162
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
179
163
|
): Promise<void> {
|
|
180
|
-
const { threadId, content, timestamp, metadata } = message;
|
|
181
164
|
const pairing = createScopedPairingAccess({
|
|
182
165
|
core,
|
|
183
166
|
channel: "zalouser",
|
|
184
167
|
accountId: account.accountId,
|
|
185
168
|
});
|
|
186
|
-
|
|
169
|
+
|
|
170
|
+
const rawBody = message.content?.trim();
|
|
171
|
+
if (!rawBody) {
|
|
187
172
|
return;
|
|
188
173
|
}
|
|
189
174
|
|
|
190
|
-
const isGroup =
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
175
|
+
const isGroup = message.isGroup;
|
|
176
|
+
const chatId = message.threadId;
|
|
177
|
+
const senderId = message.senderId?.trim();
|
|
178
|
+
if (!senderId) {
|
|
179
|
+
logVerbose(core, runtime, `zalouser: drop message ${chatId} (missing senderId)`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const senderName = message.senderName ?? "";
|
|
183
|
+
const configuredGroupName = message.groupName?.trim() || "";
|
|
184
|
+
const groupContext =
|
|
185
|
+
isGroup && !configuredGroupName
|
|
186
|
+
? await resolveZaloGroupContext(account.profile, chatId).catch((err) => {
|
|
187
|
+
logVerbose(
|
|
188
|
+
core,
|
|
189
|
+
runtime,
|
|
190
|
+
`zalouser: group context lookup failed for ${chatId}: ${String(err)}`,
|
|
191
|
+
);
|
|
192
|
+
return null;
|
|
193
|
+
})
|
|
194
|
+
: null;
|
|
195
|
+
const groupName = configuredGroupName || groupContext?.name?.trim() || "";
|
|
196
|
+
const groupMembers = groupContext?.members?.slice(0, 20).join(", ") || undefined;
|
|
197
|
+
|
|
198
|
+
if (message.eventMessage) {
|
|
199
|
+
try {
|
|
200
|
+
await sendZalouserDeliveryAcks({
|
|
201
|
+
profile: account.profile,
|
|
202
|
+
isGroup,
|
|
203
|
+
message: message.eventMessage,
|
|
204
|
+
});
|
|
205
|
+
} catch (err) {
|
|
206
|
+
logVerbose(core, runtime, `zalouser: delivery/seen ack failed for ${chatId}: ${String(err)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
195
209
|
|
|
196
210
|
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
197
211
|
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
|
@@ -203,8 +217,9 @@ async function processMessage(
|
|
|
203
217
|
providerMissingFallbackApplied,
|
|
204
218
|
providerKey: "zalouser",
|
|
205
219
|
accountId: account.accountId,
|
|
206
|
-
log: (
|
|
220
|
+
log: (entry) => logVerbose(core, runtime, entry),
|
|
207
221
|
});
|
|
222
|
+
|
|
208
223
|
const groups = account.config.groups ?? {};
|
|
209
224
|
if (isGroup) {
|
|
210
225
|
if (groupPolicy === "disabled") {
|
|
@@ -222,7 +237,6 @@ async function processMessage(
|
|
|
222
237
|
|
|
223
238
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
224
239
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
225
|
-
const rawBody = content.trim();
|
|
226
240
|
const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorization({
|
|
227
241
|
cfg: config,
|
|
228
242
|
rawBody,
|
|
@@ -246,7 +260,6 @@ async function processMessage(
|
|
|
246
260
|
|
|
247
261
|
if (dmPolicy !== "open") {
|
|
248
262
|
const allowed = senderAllowedForCommands;
|
|
249
|
-
|
|
250
263
|
if (!allowed) {
|
|
251
264
|
if (dmPolicy === "pairing") {
|
|
252
265
|
const { code, created } = await pairing.upsertPairingRequest({
|
|
@@ -287,11 +300,8 @@ async function processMessage(
|
|
|
287
300
|
}
|
|
288
301
|
}
|
|
289
302
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
core.channel.commands.isControlCommandMessage(rawBody, config) &&
|
|
293
|
-
commandAuthorized !== true
|
|
294
|
-
) {
|
|
303
|
+
const hasControlCommand = core.channel.commands.isControlCommandMessage(rawBody, config);
|
|
304
|
+
if (isGroup && hasControlCommand && commandAuthorized !== true) {
|
|
295
305
|
logVerbose(
|
|
296
306
|
core,
|
|
297
307
|
runtime,
|
|
@@ -315,7 +325,46 @@ async function processMessage(
|
|
|
315
325
|
},
|
|
316
326
|
});
|
|
317
327
|
|
|
318
|
-
const
|
|
328
|
+
const requireMention = isGroup
|
|
329
|
+
? resolveGroupRequireMention({
|
|
330
|
+
groupId: chatId,
|
|
331
|
+
groupName,
|
|
332
|
+
groups,
|
|
333
|
+
})
|
|
334
|
+
: false;
|
|
335
|
+
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
|
|
336
|
+
const explicitMention = {
|
|
337
|
+
hasAnyMention: message.hasAnyMention === true,
|
|
338
|
+
isExplicitlyMentioned: message.wasExplicitlyMentioned === true,
|
|
339
|
+
canResolveExplicit: message.canResolveExplicitMention === true,
|
|
340
|
+
};
|
|
341
|
+
const wasMentioned = isGroup
|
|
342
|
+
? core.channel.mentions.matchesMentionWithExplicit({
|
|
343
|
+
text: rawBody,
|
|
344
|
+
mentionRegexes,
|
|
345
|
+
explicit: explicitMention,
|
|
346
|
+
})
|
|
347
|
+
: true;
|
|
348
|
+
const mentionGate = resolveMentionGatingWithBypass({
|
|
349
|
+
isGroup,
|
|
350
|
+
requireMention,
|
|
351
|
+
canDetectMention: mentionRegexes.length > 0 || explicitMention.canResolveExplicit,
|
|
352
|
+
wasMentioned,
|
|
353
|
+
implicitMention: message.implicitMention === true,
|
|
354
|
+
hasAnyMention: explicitMention.hasAnyMention,
|
|
355
|
+
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
|
356
|
+
cfg: config,
|
|
357
|
+
surface: "zalouser",
|
|
358
|
+
}),
|
|
359
|
+
hasControlCommand,
|
|
360
|
+
commandAuthorized: commandAuthorized === true,
|
|
361
|
+
});
|
|
362
|
+
if (isGroup && mentionGate.shouldSkip) {
|
|
363
|
+
logVerbose(core, runtime, `zalouser: skip group ${chatId} (mention required, not mentioned)`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const fromLabel = isGroup ? groupName || `group:${chatId}` : senderName || `user:${senderId}`;
|
|
319
368
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
320
369
|
agentId: route.agentId,
|
|
321
370
|
});
|
|
@@ -327,7 +376,7 @@ async function processMessage(
|
|
|
327
376
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
328
377
|
channel: "Zalo Personal",
|
|
329
378
|
from: fromLabel,
|
|
330
|
-
timestamp:
|
|
379
|
+
timestamp: message.timestampMs,
|
|
331
380
|
previousTimestamp,
|
|
332
381
|
envelope: envelopeOptions,
|
|
333
382
|
body: rawBody,
|
|
@@ -344,12 +393,24 @@ async function processMessage(
|
|
|
344
393
|
AccountId: route.accountId,
|
|
345
394
|
ChatType: isGroup ? "group" : "direct",
|
|
346
395
|
ConversationLabel: fromLabel,
|
|
396
|
+
GroupSubject: isGroup ? groupName || undefined : undefined,
|
|
397
|
+
GroupChannel: isGroup ? groupName || undefined : undefined,
|
|
398
|
+
GroupMembers: isGroup ? groupMembers : undefined,
|
|
347
399
|
SenderName: senderName || undefined,
|
|
348
400
|
SenderId: senderId,
|
|
401
|
+
WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : undefined,
|
|
349
402
|
CommandAuthorized: commandAuthorized,
|
|
350
403
|
Provider: "zalouser",
|
|
351
404
|
Surface: "zalouser",
|
|
352
|
-
MessageSid:
|
|
405
|
+
MessageSid: resolveZalouserMessageSid({
|
|
406
|
+
msgId: message.msgId,
|
|
407
|
+
cliMsgId: message.cliMsgId,
|
|
408
|
+
fallback: `${message.timestampMs}`,
|
|
409
|
+
}),
|
|
410
|
+
MessageSidFull: formatZalouserMessageSidFull({
|
|
411
|
+
msgId: message.msgId,
|
|
412
|
+
cliMsgId: message.cliMsgId,
|
|
413
|
+
}),
|
|
353
414
|
OriginatingChannel: "zalouser",
|
|
354
415
|
OriginatingTo: `zalouser:${chatId}`,
|
|
355
416
|
});
|
|
@@ -369,12 +430,24 @@ async function processMessage(
|
|
|
369
430
|
channel: "zalouser",
|
|
370
431
|
accountId: account.accountId,
|
|
371
432
|
});
|
|
433
|
+
const typingCallbacks = createTypingCallbacks({
|
|
434
|
+
start: async () => {
|
|
435
|
+
await sendTypingZalouser(chatId, {
|
|
436
|
+
profile: account.profile,
|
|
437
|
+
isGroup,
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
onStartError: (err) => {
|
|
441
|
+
logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
|
|
442
|
+
},
|
|
443
|
+
});
|
|
372
444
|
|
|
373
445
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
374
446
|
ctx: ctxPayload,
|
|
375
447
|
cfg: config,
|
|
376
448
|
dispatcherOptions: {
|
|
377
449
|
...prefixOptions,
|
|
450
|
+
typingCallbacks,
|
|
378
451
|
deliver: async (payload) => {
|
|
379
452
|
await deliverZalouserReply({
|
|
380
453
|
payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
|
|
@@ -466,10 +539,6 @@ export async function monitorZalouserProvider(
|
|
|
466
539
|
const { abortSignal, statusSink, runtime } = options;
|
|
467
540
|
|
|
468
541
|
const core = getZalouserRuntime();
|
|
469
|
-
let stopped = false;
|
|
470
|
-
let proc: ChildProcess | null = null;
|
|
471
|
-
let restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
472
|
-
let resolveRunning: (() => void) | null = null;
|
|
473
542
|
|
|
474
543
|
try {
|
|
475
544
|
const profile = account.profile;
|
|
@@ -478,147 +547,144 @@ export async function monitorZalouserProvider(
|
|
|
478
547
|
.filter((entry) => entry && entry !== "*");
|
|
479
548
|
|
|
480
549
|
if (allowFromEntries.length > 0) {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
} else {
|
|
500
|
-
unresolved.push(entry);
|
|
501
|
-
}
|
|
550
|
+
const friends = await listZaloFriends(profile);
|
|
551
|
+
const byName = buildNameIndex(friends, (friend) => friend.displayName);
|
|
552
|
+
const additions: string[] = [];
|
|
553
|
+
const mapping: string[] = [];
|
|
554
|
+
const unresolved: string[] = [];
|
|
555
|
+
for (const entry of allowFromEntries) {
|
|
556
|
+
if (/^\d+$/.test(entry)) {
|
|
557
|
+
additions.push(entry);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const matches = byName.get(entry.toLowerCase()) ?? [];
|
|
561
|
+
const match = matches[0];
|
|
562
|
+
const id = match?.userId ? String(match.userId) : undefined;
|
|
563
|
+
if (id) {
|
|
564
|
+
additions.push(id);
|
|
565
|
+
mapping.push(`${entry}→${id}`);
|
|
566
|
+
} else {
|
|
567
|
+
unresolved.push(entry);
|
|
502
568
|
}
|
|
503
|
-
const allowFrom = mergeAllowlist({ existing: account.config.allowFrom, additions });
|
|
504
|
-
account = {
|
|
505
|
-
...account,
|
|
506
|
-
config: {
|
|
507
|
-
...account.config,
|
|
508
|
-
allowFrom,
|
|
509
|
-
},
|
|
510
|
-
};
|
|
511
|
-
summarizeMapping("zalouser users", mapping, unresolved, runtime);
|
|
512
|
-
} else {
|
|
513
|
-
runtime.log?.(`zalouser user resolve failed; using config entries. ${result.stderr}`);
|
|
514
569
|
}
|
|
570
|
+
const allowFrom = mergeAllowlist({ existing: account.config.allowFrom, additions });
|
|
571
|
+
account = {
|
|
572
|
+
...account,
|
|
573
|
+
config: {
|
|
574
|
+
...account.config,
|
|
575
|
+
allowFrom,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
summarizeMapping("zalouser users", mapping, unresolved, runtime);
|
|
515
579
|
}
|
|
516
580
|
|
|
517
581
|
const groupsConfig = account.config.groups ?? {};
|
|
518
582
|
const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
|
|
519
583
|
if (groupKeys.length > 0) {
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (!nextGroups[cleaned]) {
|
|
531
|
-
nextGroups[cleaned] = groupsConfig[entry];
|
|
532
|
-
}
|
|
533
|
-
mapping.push(`${entry}→${cleaned}`);
|
|
534
|
-
continue;
|
|
584
|
+
const groups = await listZaloGroups(profile);
|
|
585
|
+
const byName = buildNameIndex(groups, (group) => group.name);
|
|
586
|
+
const mapping: string[] = [];
|
|
587
|
+
const unresolved: string[] = [];
|
|
588
|
+
const nextGroups = { ...groupsConfig };
|
|
589
|
+
for (const entry of groupKeys) {
|
|
590
|
+
const cleaned = normalizeZalouserEntry(entry);
|
|
591
|
+
if (/^\d+$/.test(cleaned)) {
|
|
592
|
+
if (!nextGroups[cleaned]) {
|
|
593
|
+
nextGroups[cleaned] = groupsConfig[entry];
|
|
535
594
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
unresolved.push(entry);
|
|
595
|
+
mapping.push(`${entry}→${cleaned}`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const matches = byName.get(cleaned.toLowerCase()) ?? [];
|
|
599
|
+
const match = matches[0];
|
|
600
|
+
const id = match?.groupId ? String(match.groupId) : undefined;
|
|
601
|
+
if (id) {
|
|
602
|
+
if (!nextGroups[id]) {
|
|
603
|
+
nextGroups[id] = groupsConfig[entry];
|
|
546
604
|
}
|
|
605
|
+
mapping.push(`${entry}→${id}`);
|
|
606
|
+
} else {
|
|
607
|
+
unresolved.push(entry);
|
|
547
608
|
}
|
|
548
|
-
account = {
|
|
549
|
-
...account,
|
|
550
|
-
config: {
|
|
551
|
-
...account.config,
|
|
552
|
-
groups: nextGroups,
|
|
553
|
-
},
|
|
554
|
-
};
|
|
555
|
-
summarizeMapping("zalouser groups", mapping, unresolved, runtime);
|
|
556
|
-
} else {
|
|
557
|
-
runtime.log?.(`zalouser group resolve failed; using config entries. ${result.stderr}`);
|
|
558
609
|
}
|
|
610
|
+
account = {
|
|
611
|
+
...account,
|
|
612
|
+
config: {
|
|
613
|
+
...account.config,
|
|
614
|
+
groups: nextGroups,
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
summarizeMapping("zalouser groups", mapping, unresolved, runtime);
|
|
559
618
|
}
|
|
560
619
|
} catch (err) {
|
|
561
620
|
runtime.log?.(`zalouser resolve failed; using config entries. ${String(err)}`);
|
|
562
621
|
}
|
|
563
622
|
|
|
623
|
+
let listenerStop: (() => void) | null = null;
|
|
624
|
+
let stopped = false;
|
|
625
|
+
|
|
564
626
|
const stop = () => {
|
|
565
|
-
stopped
|
|
566
|
-
|
|
567
|
-
clearTimeout(restartTimer);
|
|
568
|
-
restartTimer = null;
|
|
569
|
-
}
|
|
570
|
-
if (proc) {
|
|
571
|
-
proc.kill("SIGTERM");
|
|
572
|
-
proc = null;
|
|
627
|
+
if (stopped) {
|
|
628
|
+
return;
|
|
573
629
|
}
|
|
574
|
-
|
|
630
|
+
stopped = true;
|
|
631
|
+
listenerStop?.();
|
|
632
|
+
listenerStop = null;
|
|
575
633
|
};
|
|
576
634
|
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
635
|
+
const listener = await startZaloListener({
|
|
636
|
+
accountId: account.accountId,
|
|
637
|
+
profile: account.profile,
|
|
638
|
+
abortSignal,
|
|
639
|
+
onMessage: (msg) => {
|
|
640
|
+
if (stopped) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
logVerbose(core, runtime, `[${account.accountId}] inbound message`);
|
|
644
|
+
statusSink?.({ lastInboundAt: Date.now() });
|
|
645
|
+
processMessage(msg, account, config, core, runtime, statusSink).catch((err) => {
|
|
646
|
+
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
|
647
|
+
});
|
|
648
|
+
},
|
|
649
|
+
onError: (err) => {
|
|
650
|
+
if (stopped || abortSignal.aborted) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
runtime.error(`[${account.accountId}] Zalo listener error: ${String(err)}`);
|
|
654
|
+
},
|
|
655
|
+
});
|
|
582
656
|
|
|
583
|
-
|
|
584
|
-
core,
|
|
585
|
-
runtime,
|
|
586
|
-
`[${account.accountId}] starting zca listener (profile=${account.profile})`,
|
|
587
|
-
);
|
|
657
|
+
listenerStop = listener.stop;
|
|
588
658
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
processMessage(msg, account, config, core, runtime, statusSink).catch((err) => {
|
|
596
|
-
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
|
597
|
-
});
|
|
659
|
+
await new Promise<void>((resolve) => {
|
|
660
|
+
abortSignal.addEventListener(
|
|
661
|
+
"abort",
|
|
662
|
+
() => {
|
|
663
|
+
stop();
|
|
664
|
+
resolve();
|
|
598
665
|
},
|
|
599
|
-
|
|
600
|
-
runtime.error(`[${account.accountId}] zca listener error: ${String(err)}`);
|
|
601
|
-
if (!stopped && !abortSignal.aborted) {
|
|
602
|
-
logVerbose(core, runtime, `[${account.accountId}] restarting listener in 5s...`);
|
|
603
|
-
restartTimer = setTimeout(startListener, 5000);
|
|
604
|
-
} else {
|
|
605
|
-
resolveRunning?.();
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
abortSignal,
|
|
666
|
+
{ once: true },
|
|
609
667
|
);
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
// Create a promise that stays pending until abort or stop
|
|
613
|
-
const runningPromise = new Promise<void>((resolve) => {
|
|
614
|
-
resolveRunning = resolve;
|
|
615
|
-
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
616
668
|
});
|
|
617
669
|
|
|
618
|
-
startListener();
|
|
619
|
-
|
|
620
|
-
// Wait for the running promise to resolve (on abort/stop)
|
|
621
|
-
await runningPromise;
|
|
622
|
-
|
|
623
670
|
return { stop };
|
|
624
671
|
}
|
|
672
|
+
|
|
673
|
+
export const __testing = {
|
|
674
|
+
processMessage: async (params: {
|
|
675
|
+
message: ZaloInboundMessage;
|
|
676
|
+
account: ResolvedZalouserAccount;
|
|
677
|
+
config: OpenClawConfig;
|
|
678
|
+
runtime: RuntimeEnv;
|
|
679
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
680
|
+
}) => {
|
|
681
|
+
await processMessage(
|
|
682
|
+
params.message,
|
|
683
|
+
params.account,
|
|
684
|
+
params.config,
|
|
685
|
+
getZalouserRuntime(),
|
|
686
|
+
params.runtime,
|
|
687
|
+
params.statusSink,
|
|
688
|
+
);
|
|
689
|
+
},
|
|
690
|
+
};
|