@openclaw/zalouser 2026.2.25 → 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 +22 -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 +28 -17
- 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 +299 -228
- 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,9 +5,12 @@ import type {
|
|
|
6
5
|
RuntimeEnv,
|
|
7
6
|
} from "openclaw/plugin-sdk";
|
|
8
7
|
import {
|
|
8
|
+
createTypingCallbacks,
|
|
9
|
+
createScopedPairingAccess,
|
|
9
10
|
createReplyPrefixOptions,
|
|
10
11
|
resolveOutboundMediaUrls,
|
|
11
12
|
mergeAllowlist,
|
|
13
|
+
resolveMentionGatingWithBypass,
|
|
12
14
|
resolveOpenProviderRuntimeGroupPolicy,
|
|
13
15
|
resolveDefaultGroupPolicy,
|
|
14
16
|
resolveSenderCommandAuthorization,
|
|
@@ -16,10 +18,26 @@ import {
|
|
|
16
18
|
summarizeMapping,
|
|
17
19
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
18
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";
|
|
19
27
|
import { getZalouserRuntime } from "./runtime.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
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";
|
|
23
41
|
|
|
24
42
|
export type ZalouserMonitorOptions = {
|
|
25
43
|
account: ResolvedZalouserAccount;
|
|
@@ -61,131 +79,133 @@ function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: str
|
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
|
82
|
+
function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boolean {
|
|
65
83
|
if (allowFrom.includes("*")) {
|
|
66
84
|
return true;
|
|
67
85
|
}
|
|
68
|
-
const normalizedSenderId = senderId.toLowerCase();
|
|
86
|
+
const normalizedSenderId = senderId?.trim().toLowerCase();
|
|
87
|
+
if (!normalizedSenderId) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
69
90
|
return allowFrom.some((entry) => {
|
|
70
91
|
const normalized = entry.toLowerCase().replace(/^(zalouser|zlu):/i, "");
|
|
71
92
|
return normalized === normalizedSenderId;
|
|
72
93
|
});
|
|
73
94
|
}
|
|
74
95
|
|
|
75
|
-
function normalizeGroupSlug(raw?: string | null): string {
|
|
76
|
-
const trimmed = raw?.trim().toLowerCase() ?? "";
|
|
77
|
-
if (!trimmed) {
|
|
78
|
-
return "";
|
|
79
|
-
}
|
|
80
|
-
return trimmed
|
|
81
|
-
.replace(/^#/, "")
|
|
82
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
83
|
-
.replace(/^-+|-+$/g, "");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
96
|
function isGroupAllowed(params: {
|
|
87
97
|
groupId: string;
|
|
88
98
|
groupName?: string | null;
|
|
89
|
-
groups: Record<string, { allow?: boolean; enabled?: boolean }>;
|
|
99
|
+
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
|
90
100
|
}): boolean {
|
|
91
101
|
const groups = params.groups ?? {};
|
|
92
102
|
const keys = Object.keys(groups);
|
|
93
103
|
if (keys.length === 0) {
|
|
94
104
|
return false;
|
|
95
105
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
return entry.allow !== false && entry.enabled !== false;
|
|
108
|
-
}
|
|
109
|
-
const wildcard = groups["*"];
|
|
110
|
-
if (wildcard) {
|
|
111
|
-
return wildcard.allow !== false && wildcard.enabled !== false;
|
|
112
|
-
}
|
|
113
|
-
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);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(trimmed) as ZcaMessage;
|
|
138
|
-
onMessage(parsed);
|
|
139
|
-
} catch {
|
|
140
|
-
// ignore non-JSON lines
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
onError,
|
|
145
|
-
});
|
|
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
|
+
}
|
|
146
137
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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,
|
|
152
148
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
149
|
+
await sendSeenZalouser({
|
|
150
|
+
profile: params.profile,
|
|
151
|
+
isGroup: params.isGroup,
|
|
152
|
+
message: params.message,
|
|
158
153
|
});
|
|
159
|
-
|
|
160
|
-
abortSignal.addEventListener(
|
|
161
|
-
"abort",
|
|
162
|
-
() => {
|
|
163
|
-
proc.kill("SIGTERM");
|
|
164
|
-
},
|
|
165
|
-
{ once: true },
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
return proc;
|
|
169
154
|
}
|
|
170
155
|
|
|
171
156
|
async function processMessage(
|
|
172
|
-
message:
|
|
157
|
+
message: ZaloInboundMessage,
|
|
173
158
|
account: ResolvedZalouserAccount,
|
|
174
159
|
config: OpenClawConfig,
|
|
175
160
|
core: ZalouserCoreRuntime,
|
|
176
161
|
runtime: RuntimeEnv,
|
|
177
162
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
178
163
|
): Promise<void> {
|
|
179
|
-
const
|
|
180
|
-
|
|
164
|
+
const pairing = createScopedPairingAccess({
|
|
165
|
+
core,
|
|
166
|
+
channel: "zalouser",
|
|
167
|
+
accountId: account.accountId,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const rawBody = message.content?.trim();
|
|
171
|
+
if (!rawBody) {
|
|
181
172
|
return;
|
|
182
173
|
}
|
|
183
174
|
|
|
184
|
-
const isGroup =
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
209
|
|
|
190
210
|
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
191
211
|
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
|
@@ -197,8 +217,9 @@ async function processMessage(
|
|
|
197
217
|
providerMissingFallbackApplied,
|
|
198
218
|
providerKey: "zalouser",
|
|
199
219
|
accountId: account.accountId,
|
|
200
|
-
log: (
|
|
220
|
+
log: (entry) => logVerbose(core, runtime, entry),
|
|
201
221
|
});
|
|
222
|
+
|
|
202
223
|
const groups = account.config.groups ?? {};
|
|
203
224
|
if (isGroup) {
|
|
204
225
|
if (groupPolicy === "disabled") {
|
|
@@ -216,7 +237,6 @@ async function processMessage(
|
|
|
216
237
|
|
|
217
238
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
218
239
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
219
|
-
const rawBody = content.trim();
|
|
220
240
|
const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorization({
|
|
221
241
|
cfg: config,
|
|
222
242
|
rawBody,
|
|
@@ -225,7 +245,7 @@ async function processMessage(
|
|
|
225
245
|
configuredAllowFrom: configAllowFrom,
|
|
226
246
|
senderId,
|
|
227
247
|
isSenderAllowed,
|
|
228
|
-
readAllowFromStore:
|
|
248
|
+
readAllowFromStore: pairing.readAllowFromStore,
|
|
229
249
|
shouldComputeCommandAuthorized: (body, cfg) =>
|
|
230
250
|
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
|
231
251
|
resolveCommandAuthorizedFromAuthorizers: (params) =>
|
|
@@ -240,11 +260,9 @@ async function processMessage(
|
|
|
240
260
|
|
|
241
261
|
if (dmPolicy !== "open") {
|
|
242
262
|
const allowed = senderAllowedForCommands;
|
|
243
|
-
|
|
244
263
|
if (!allowed) {
|
|
245
264
|
if (dmPolicy === "pairing") {
|
|
246
|
-
const { code, created } = await
|
|
247
|
-
channel: "zalouser",
|
|
265
|
+
const { code, created } = await pairing.upsertPairingRequest({
|
|
248
266
|
id: senderId,
|
|
249
267
|
meta: { name: senderName || undefined },
|
|
250
268
|
});
|
|
@@ -282,11 +300,8 @@ async function processMessage(
|
|
|
282
300
|
}
|
|
283
301
|
}
|
|
284
302
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
core.channel.commands.isControlCommandMessage(rawBody, config) &&
|
|
288
|
-
commandAuthorized !== true
|
|
289
|
-
) {
|
|
303
|
+
const hasControlCommand = core.channel.commands.isControlCommandMessage(rawBody, config);
|
|
304
|
+
if (isGroup && hasControlCommand && commandAuthorized !== true) {
|
|
290
305
|
logVerbose(
|
|
291
306
|
core,
|
|
292
307
|
runtime,
|
|
@@ -310,7 +325,46 @@ async function processMessage(
|
|
|
310
325
|
},
|
|
311
326
|
});
|
|
312
327
|
|
|
313
|
-
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}`;
|
|
314
368
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
315
369
|
agentId: route.agentId,
|
|
316
370
|
});
|
|
@@ -322,7 +376,7 @@ async function processMessage(
|
|
|
322
376
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
323
377
|
channel: "Zalo Personal",
|
|
324
378
|
from: fromLabel,
|
|
325
|
-
timestamp:
|
|
379
|
+
timestamp: message.timestampMs,
|
|
326
380
|
previousTimestamp,
|
|
327
381
|
envelope: envelopeOptions,
|
|
328
382
|
body: rawBody,
|
|
@@ -339,12 +393,24 @@ async function processMessage(
|
|
|
339
393
|
AccountId: route.accountId,
|
|
340
394
|
ChatType: isGroup ? "group" : "direct",
|
|
341
395
|
ConversationLabel: fromLabel,
|
|
396
|
+
GroupSubject: isGroup ? groupName || undefined : undefined,
|
|
397
|
+
GroupChannel: isGroup ? groupName || undefined : undefined,
|
|
398
|
+
GroupMembers: isGroup ? groupMembers : undefined,
|
|
342
399
|
SenderName: senderName || undefined,
|
|
343
400
|
SenderId: senderId,
|
|
401
|
+
WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : undefined,
|
|
344
402
|
CommandAuthorized: commandAuthorized,
|
|
345
403
|
Provider: "zalouser",
|
|
346
404
|
Surface: "zalouser",
|
|
347
|
-
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
|
+
}),
|
|
348
414
|
OriginatingChannel: "zalouser",
|
|
349
415
|
OriginatingTo: `zalouser:${chatId}`,
|
|
350
416
|
});
|
|
@@ -364,12 +430,24 @@ async function processMessage(
|
|
|
364
430
|
channel: "zalouser",
|
|
365
431
|
accountId: account.accountId,
|
|
366
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
|
+
});
|
|
367
444
|
|
|
368
445
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
369
446
|
ctx: ctxPayload,
|
|
370
447
|
cfg: config,
|
|
371
448
|
dispatcherOptions: {
|
|
372
449
|
...prefixOptions,
|
|
450
|
+
typingCallbacks,
|
|
373
451
|
deliver: async (payload) => {
|
|
374
452
|
await deliverZalouserReply({
|
|
375
453
|
payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
|
|
@@ -461,10 +539,6 @@ export async function monitorZalouserProvider(
|
|
|
461
539
|
const { abortSignal, statusSink, runtime } = options;
|
|
462
540
|
|
|
463
541
|
const core = getZalouserRuntime();
|
|
464
|
-
let stopped = false;
|
|
465
|
-
let proc: ChildProcess | null = null;
|
|
466
|
-
let restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
467
|
-
let resolveRunning: (() => void) | null = null;
|
|
468
542
|
|
|
469
543
|
try {
|
|
470
544
|
const profile = account.profile;
|
|
@@ -473,147 +547,144 @@ export async function monitorZalouserProvider(
|
|
|
473
547
|
.filter((entry) => entry && entry !== "*");
|
|
474
548
|
|
|
475
549
|
if (allowFromEntries.length > 0) {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
} else {
|
|
495
|
-
unresolved.push(entry);
|
|
496
|
-
}
|
|
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);
|
|
497
568
|
}
|
|
498
|
-
const allowFrom = mergeAllowlist({ existing: account.config.allowFrom, additions });
|
|
499
|
-
account = {
|
|
500
|
-
...account,
|
|
501
|
-
config: {
|
|
502
|
-
...account.config,
|
|
503
|
-
allowFrom,
|
|
504
|
-
},
|
|
505
|
-
};
|
|
506
|
-
summarizeMapping("zalouser users", mapping, unresolved, runtime);
|
|
507
|
-
} else {
|
|
508
|
-
runtime.log?.(`zalouser user resolve failed; using config entries. ${result.stderr}`);
|
|
509
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);
|
|
510
579
|
}
|
|
511
580
|
|
|
512
581
|
const groupsConfig = account.config.groups ?? {};
|
|
513
582
|
const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
|
|
514
583
|
if (groupKeys.length > 0) {
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (!nextGroups[cleaned]) {
|
|
526
|
-
nextGroups[cleaned] = groupsConfig[entry];
|
|
527
|
-
}
|
|
528
|
-
mapping.push(`${entry}→${cleaned}`);
|
|
529
|
-
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];
|
|
530
594
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
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];
|
|
541
604
|
}
|
|
605
|
+
mapping.push(`${entry}→${id}`);
|
|
606
|
+
} else {
|
|
607
|
+
unresolved.push(entry);
|
|
542
608
|
}
|
|
543
|
-
account = {
|
|
544
|
-
...account,
|
|
545
|
-
config: {
|
|
546
|
-
...account.config,
|
|
547
|
-
groups: nextGroups,
|
|
548
|
-
},
|
|
549
|
-
};
|
|
550
|
-
summarizeMapping("zalouser groups", mapping, unresolved, runtime);
|
|
551
|
-
} else {
|
|
552
|
-
runtime.log?.(`zalouser group resolve failed; using config entries. ${result.stderr}`);
|
|
553
609
|
}
|
|
610
|
+
account = {
|
|
611
|
+
...account,
|
|
612
|
+
config: {
|
|
613
|
+
...account.config,
|
|
614
|
+
groups: nextGroups,
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
summarizeMapping("zalouser groups", mapping, unresolved, runtime);
|
|
554
618
|
}
|
|
555
619
|
} catch (err) {
|
|
556
620
|
runtime.log?.(`zalouser resolve failed; using config entries. ${String(err)}`);
|
|
557
621
|
}
|
|
558
622
|
|
|
623
|
+
let listenerStop: (() => void) | null = null;
|
|
624
|
+
let stopped = false;
|
|
625
|
+
|
|
559
626
|
const stop = () => {
|
|
560
|
-
stopped
|
|
561
|
-
|
|
562
|
-
clearTimeout(restartTimer);
|
|
563
|
-
restartTimer = null;
|
|
564
|
-
}
|
|
565
|
-
if (proc) {
|
|
566
|
-
proc.kill("SIGTERM");
|
|
567
|
-
proc = null;
|
|
627
|
+
if (stopped) {
|
|
628
|
+
return;
|
|
568
629
|
}
|
|
569
|
-
|
|
630
|
+
stopped = true;
|
|
631
|
+
listenerStop?.();
|
|
632
|
+
listenerStop = null;
|
|
570
633
|
};
|
|
571
634
|
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
});
|
|
577
656
|
|
|
578
|
-
|
|
579
|
-
core,
|
|
580
|
-
runtime,
|
|
581
|
-
`[${account.accountId}] starting zca listener (profile=${account.profile})`,
|
|
582
|
-
);
|
|
657
|
+
listenerStop = listener.stop;
|
|
583
658
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
processMessage(msg, account, config, core, runtime, statusSink).catch((err) => {
|
|
591
|
-
runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
|
|
592
|
-
});
|
|
659
|
+
await new Promise<void>((resolve) => {
|
|
660
|
+
abortSignal.addEventListener(
|
|
661
|
+
"abort",
|
|
662
|
+
() => {
|
|
663
|
+
stop();
|
|
664
|
+
resolve();
|
|
593
665
|
},
|
|
594
|
-
|
|
595
|
-
runtime.error(`[${account.accountId}] zca listener error: ${String(err)}`);
|
|
596
|
-
if (!stopped && !abortSignal.aborted) {
|
|
597
|
-
logVerbose(core, runtime, `[${account.accountId}] restarting listener in 5s...`);
|
|
598
|
-
restartTimer = setTimeout(startListener, 5000);
|
|
599
|
-
} else {
|
|
600
|
-
resolveRunning?.();
|
|
601
|
-
}
|
|
602
|
-
},
|
|
603
|
-
abortSignal,
|
|
666
|
+
{ once: true },
|
|
604
667
|
);
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
// Create a promise that stays pending until abort or stop
|
|
608
|
-
const runningPromise = new Promise<void>((resolve) => {
|
|
609
|
-
resolveRunning = resolve;
|
|
610
|
-
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
611
668
|
});
|
|
612
669
|
|
|
613
|
-
startListener();
|
|
614
|
-
|
|
615
|
-
// Wait for the running promise to resolve (on abort/stop)
|
|
616
|
-
await runningPromise;
|
|
617
|
-
|
|
618
670
|
return { stop };
|
|
619
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
|
+
};
|