@openclaw/zalouser 2026.3.1 → 2026.3.7
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 +3 -5
- package/package.json +5 -3
- package/src/accounts.test.ts +214 -0
- package/src/accounts.ts +24 -51
- package/src/channel.sendpayload.test.ts +117 -0
- package/src/channel.test.ts +123 -1
- package/src/channel.ts +232 -272
- package/src/config-schema.ts +2 -1
- 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 +113 -0
- package/src/monitor.group-gating.test.ts +211 -0
- package/src/monitor.send-mocks.ts +20 -0
- package/src/monitor.ts +321 -260
- package/src/onboarding.ts +106 -171
- package/src/probe.test.ts +60 -0
- package/src/probe.ts +20 -13
- package/src/qr-temp-file.ts +22 -0
- package/src/reaction.test.ts +19 -0
- package/src/reaction.ts +29 -0
- package/src/runtime.ts +1 -1
- 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 +8 -27
- 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 +216 -0
- package/src/zca-js-exports.d.ts +22 -0
- package/src/zca.ts +0 -198
package/src/monitor.ts
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
|
-
import type { ChildProcess } from "node:child_process";
|
|
2
1
|
import type {
|
|
3
2
|
MarkdownTableMode,
|
|
4
3
|
OpenClawConfig,
|
|
5
4
|
OutboundReplyPayload,
|
|
6
5
|
RuntimeEnv,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
+
} from "openclaw/plugin-sdk/zalouser";
|
|
8
7
|
import {
|
|
8
|
+
createTypingCallbacks,
|
|
9
9
|
createScopedPairingAccess,
|
|
10
10
|
createReplyPrefixOptions,
|
|
11
|
+
evaluateGroupRouteAccessForPolicy,
|
|
12
|
+
issuePairingChallenge,
|
|
11
13
|
resolveOutboundMediaUrls,
|
|
12
14
|
mergeAllowlist,
|
|
15
|
+
resolveMentionGatingWithBypass,
|
|
13
16
|
resolveOpenProviderRuntimeGroupPolicy,
|
|
14
17
|
resolveDefaultGroupPolicy,
|
|
15
18
|
resolveSenderCommandAuthorization,
|
|
16
19
|
sendMediaWithLeadingCaption,
|
|
17
20
|
summarizeMapping,
|
|
18
21
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
19
|
-
} from "openclaw/plugin-sdk";
|
|
22
|
+
} from "openclaw/plugin-sdk/zalouser";
|
|
23
|
+
import {
|
|
24
|
+
buildZalouserGroupCandidates,
|
|
25
|
+
findZalouserGroupEntry,
|
|
26
|
+
isZalouserGroupEntryAllowed,
|
|
27
|
+
} from "./group-policy.js";
|
|
28
|
+
import { formatZalouserMessageSidFull, resolveZalouserMessageSid } from "./message-sid.js";
|
|
20
29
|
import { getZalouserRuntime } from "./runtime.js";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
import {
|
|
31
|
+
sendDeliveredZalouser,
|
|
32
|
+
sendMessageZalouser,
|
|
33
|
+
sendSeenZalouser,
|
|
34
|
+
sendTypingZalouser,
|
|
35
|
+
} from "./send.js";
|
|
36
|
+
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
|
|
37
|
+
import {
|
|
38
|
+
listZaloFriends,
|
|
39
|
+
listZaloGroups,
|
|
40
|
+
resolveZaloGroupContext,
|
|
41
|
+
startZaloListener,
|
|
42
|
+
} from "./zalo-js.js";
|
|
24
43
|
|
|
25
44
|
export type ZalouserMonitorOptions = {
|
|
26
45
|
account: ResolvedZalouserAccount;
|
|
@@ -62,136 +81,111 @@ function logVerbose(core: ZalouserCoreRuntime, runtime: RuntimeEnv, message: str
|
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
|
84
|
+
function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boolean {
|
|
66
85
|
if (allowFrom.includes("*")) {
|
|
67
86
|
return true;
|
|
68
87
|
}
|
|
69
|
-
const normalizedSenderId = senderId.toLowerCase();
|
|
88
|
+
const normalizedSenderId = senderId?.trim().toLowerCase();
|
|
89
|
+
if (!normalizedSenderId) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
70
92
|
return allowFrom.some((entry) => {
|
|
71
93
|
const normalized = entry.toLowerCase().replace(/^(zalouser|zlu):/i, "");
|
|
72
94
|
return normalized === normalizedSenderId;
|
|
73
95
|
});
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
function
|
|
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
|
-
function isGroupAllowed(params: {
|
|
98
|
+
function resolveGroupRequireMention(params: {
|
|
88
99
|
groupId: string;
|
|
89
100
|
groupName?: string | null;
|
|
90
|
-
groups: Record<string, { allow?: boolean; enabled?: boolean }>;
|
|
101
|
+
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
|
91
102
|
}): boolean {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const candidate of candidates) {
|
|
104
|
-
const entry = groups[candidate];
|
|
105
|
-
if (!entry) {
|
|
106
|
-
continue;
|
|
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;
|
|
103
|
+
const entry = findZalouserGroupEntry(
|
|
104
|
+
params.groups ?? {},
|
|
105
|
+
buildZalouserGroupCandidates({
|
|
106
|
+
groupId: params.groupId,
|
|
107
|
+
groupName: params.groupName,
|
|
108
|
+
includeGroupIdAlias: true,
|
|
109
|
+
includeWildcard: true,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
if (typeof entry?.requireMention === "boolean") {
|
|
113
|
+
return entry.requireMention;
|
|
113
114
|
}
|
|
114
|
-
return
|
|
115
|
+
return true;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
profile,
|
|
128
|
-
onData: (chunk) => {
|
|
129
|
-
buffer += chunk;
|
|
130
|
-
const lines = buffer.split("\n");
|
|
131
|
-
buffer = lines.pop() ?? "";
|
|
132
|
-
for (const line of lines) {
|
|
133
|
-
const trimmed = line.trim();
|
|
134
|
-
if (!trimmed) {
|
|
135
|
-
continue;
|
|
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
|
-
});
|
|
147
|
-
|
|
148
|
-
proc.stderr?.on("data", (data: Buffer) => {
|
|
149
|
-
const text = data.toString().trim();
|
|
150
|
-
if (text) {
|
|
151
|
-
runtime.error(`[zalouser] zca stderr: ${text}`);
|
|
152
|
-
}
|
|
118
|
+
async function sendZalouserDeliveryAcks(params: {
|
|
119
|
+
profile: string;
|
|
120
|
+
isGroup: boolean;
|
|
121
|
+
message: NonNullable<ZaloInboundMessage["eventMessage"]>;
|
|
122
|
+
}): Promise<void> {
|
|
123
|
+
await sendDeliveredZalouser({
|
|
124
|
+
profile: params.profile,
|
|
125
|
+
isGroup: params.isGroup,
|
|
126
|
+
message: params.message,
|
|
127
|
+
isSeen: true,
|
|
153
128
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
129
|
+
await sendSeenZalouser({
|
|
130
|
+
profile: params.profile,
|
|
131
|
+
isGroup: params.isGroup,
|
|
132
|
+
message: params.message,
|
|
159
133
|
});
|
|
160
|
-
|
|
161
|
-
abortSignal.addEventListener(
|
|
162
|
-
"abort",
|
|
163
|
-
() => {
|
|
164
|
-
proc.kill("SIGTERM");
|
|
165
|
-
},
|
|
166
|
-
{ once: true },
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return proc;
|
|
170
134
|
}
|
|
171
135
|
|
|
172
136
|
async function processMessage(
|
|
173
|
-
message:
|
|
137
|
+
message: ZaloInboundMessage,
|
|
174
138
|
account: ResolvedZalouserAccount,
|
|
175
139
|
config: OpenClawConfig,
|
|
176
140
|
core: ZalouserCoreRuntime,
|
|
177
141
|
runtime: RuntimeEnv,
|
|
178
142
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
179
143
|
): Promise<void> {
|
|
180
|
-
const { threadId, content, timestamp, metadata } = message;
|
|
181
144
|
const pairing = createScopedPairingAccess({
|
|
182
145
|
core,
|
|
183
146
|
channel: "zalouser",
|
|
184
147
|
accountId: account.accountId,
|
|
185
148
|
});
|
|
186
|
-
|
|
149
|
+
|
|
150
|
+
const rawBody = message.content?.trim();
|
|
151
|
+
if (!rawBody) {
|
|
187
152
|
return;
|
|
188
153
|
}
|
|
189
154
|
|
|
190
|
-
const isGroup =
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
155
|
+
const isGroup = message.isGroup;
|
|
156
|
+
const chatId = message.threadId;
|
|
157
|
+
const senderId = message.senderId?.trim();
|
|
158
|
+
if (!senderId) {
|
|
159
|
+
logVerbose(core, runtime, `zalouser: drop message ${chatId} (missing senderId)`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const senderName = message.senderName ?? "";
|
|
163
|
+
const configuredGroupName = message.groupName?.trim() || "";
|
|
164
|
+
const groupContext =
|
|
165
|
+
isGroup && !configuredGroupName
|
|
166
|
+
? await resolveZaloGroupContext(account.profile, chatId).catch((err) => {
|
|
167
|
+
logVerbose(
|
|
168
|
+
core,
|
|
169
|
+
runtime,
|
|
170
|
+
`zalouser: group context lookup failed for ${chatId}: ${String(err)}`,
|
|
171
|
+
);
|
|
172
|
+
return null;
|
|
173
|
+
})
|
|
174
|
+
: null;
|
|
175
|
+
const groupName = configuredGroupName || groupContext?.name?.trim() || "";
|
|
176
|
+
const groupMembers = groupContext?.members?.slice(0, 20).join(", ") || undefined;
|
|
177
|
+
|
|
178
|
+
if (message.eventMessage) {
|
|
179
|
+
try {
|
|
180
|
+
await sendZalouserDeliveryAcks({
|
|
181
|
+
profile: account.profile,
|
|
182
|
+
isGroup,
|
|
183
|
+
message: message.eventMessage,
|
|
184
|
+
});
|
|
185
|
+
} catch (err) {
|
|
186
|
+
logVerbose(core, runtime, `zalouser: delivery/seen ack failed for ${chatId}: ${String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
195
189
|
|
|
196
190
|
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
197
191
|
const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
|
|
@@ -203,26 +197,46 @@ async function processMessage(
|
|
|
203
197
|
providerMissingFallbackApplied,
|
|
204
198
|
providerKey: "zalouser",
|
|
205
199
|
accountId: account.accountId,
|
|
206
|
-
log: (
|
|
200
|
+
log: (entry) => logVerbose(core, runtime, entry),
|
|
207
201
|
});
|
|
202
|
+
|
|
208
203
|
const groups = account.config.groups ?? {};
|
|
209
204
|
if (isGroup) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
205
|
+
const groupEntry = findZalouserGroupEntry(
|
|
206
|
+
groups,
|
|
207
|
+
buildZalouserGroupCandidates({
|
|
208
|
+
groupId: chatId,
|
|
209
|
+
groupName,
|
|
210
|
+
includeGroupIdAlias: true,
|
|
211
|
+
includeWildcard: true,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
|
215
|
+
groupPolicy,
|
|
216
|
+
routeAllowlistConfigured: Object.keys(groups).length > 0,
|
|
217
|
+
routeMatched: Boolean(groupEntry),
|
|
218
|
+
routeEnabled: isZalouserGroupEntryAllowed(groupEntry),
|
|
219
|
+
});
|
|
220
|
+
if (!routeAccess.allowed) {
|
|
221
|
+
if (routeAccess.reason === "disabled") {
|
|
222
|
+
logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
|
|
223
|
+
} else if (routeAccess.reason === "empty_allowlist") {
|
|
224
|
+
logVerbose(
|
|
225
|
+
core,
|
|
226
|
+
runtime,
|
|
227
|
+
`zalouser: drop group ${chatId} (groupPolicy=allowlist, no allowlist)`,
|
|
228
|
+
);
|
|
229
|
+
} else if (routeAccess.reason === "route_not_allowlisted") {
|
|
217
230
|
logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
|
|
218
|
-
|
|
231
|
+
} else if (routeAccess.reason === "route_disabled") {
|
|
232
|
+
logVerbose(core, runtime, `zalouser: drop group ${chatId} (group disabled)`);
|
|
219
233
|
}
|
|
234
|
+
return;
|
|
220
235
|
}
|
|
221
236
|
}
|
|
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,35 +260,29 @@ 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
|
-
|
|
253
|
-
|
|
265
|
+
await issuePairingChallenge({
|
|
266
|
+
channel: "zalouser",
|
|
267
|
+
senderId,
|
|
268
|
+
senderIdLine: `Your Zalo user id: ${senderId}`,
|
|
254
269
|
meta: { name: senderName || undefined },
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await sendMessageZalouser(
|
|
261
|
-
chatId,
|
|
262
|
-
core.channel.pairing.buildPairingReply({
|
|
263
|
-
channel: "zalouser",
|
|
264
|
-
idLine: `Your Zalo user id: ${senderId}`,
|
|
265
|
-
code,
|
|
266
|
-
}),
|
|
267
|
-
{ profile: account.profile },
|
|
268
|
-
);
|
|
270
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
271
|
+
onCreated: () => {
|
|
272
|
+
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
|
|
273
|
+
},
|
|
274
|
+
sendPairingReply: async (text) => {
|
|
275
|
+
await sendMessageZalouser(chatId, text, { profile: account.profile });
|
|
269
276
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
270
|
-
}
|
|
277
|
+
},
|
|
278
|
+
onReplyError: (err) => {
|
|
271
279
|
logVerbose(
|
|
272
280
|
core,
|
|
273
281
|
runtime,
|
|
274
282
|
`zalouser pairing reply failed for ${senderId}: ${String(err)}`,
|
|
275
283
|
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
284
|
+
},
|
|
285
|
+
});
|
|
278
286
|
} else {
|
|
279
287
|
logVerbose(
|
|
280
288
|
core,
|
|
@@ -287,11 +295,8 @@ async function processMessage(
|
|
|
287
295
|
}
|
|
288
296
|
}
|
|
289
297
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
core.channel.commands.isControlCommandMessage(rawBody, config) &&
|
|
293
|
-
commandAuthorized !== true
|
|
294
|
-
) {
|
|
298
|
+
const hasControlCommand = core.channel.commands.isControlCommandMessage(rawBody, config);
|
|
299
|
+
if (isGroup && hasControlCommand && commandAuthorized !== true) {
|
|
295
300
|
logVerbose(
|
|
296
301
|
core,
|
|
297
302
|
runtime,
|
|
@@ -315,7 +320,46 @@ async function processMessage(
|
|
|
315
320
|
},
|
|
316
321
|
});
|
|
317
322
|
|
|
318
|
-
const
|
|
323
|
+
const requireMention = isGroup
|
|
324
|
+
? resolveGroupRequireMention({
|
|
325
|
+
groupId: chatId,
|
|
326
|
+
groupName,
|
|
327
|
+
groups,
|
|
328
|
+
})
|
|
329
|
+
: false;
|
|
330
|
+
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
|
|
331
|
+
const explicitMention = {
|
|
332
|
+
hasAnyMention: message.hasAnyMention === true,
|
|
333
|
+
isExplicitlyMentioned: message.wasExplicitlyMentioned === true,
|
|
334
|
+
canResolveExplicit: message.canResolveExplicitMention === true,
|
|
335
|
+
};
|
|
336
|
+
const wasMentioned = isGroup
|
|
337
|
+
? core.channel.mentions.matchesMentionWithExplicit({
|
|
338
|
+
text: rawBody,
|
|
339
|
+
mentionRegexes,
|
|
340
|
+
explicit: explicitMention,
|
|
341
|
+
})
|
|
342
|
+
: true;
|
|
343
|
+
const mentionGate = resolveMentionGatingWithBypass({
|
|
344
|
+
isGroup,
|
|
345
|
+
requireMention,
|
|
346
|
+
canDetectMention: mentionRegexes.length > 0 || explicitMention.canResolveExplicit,
|
|
347
|
+
wasMentioned,
|
|
348
|
+
implicitMention: message.implicitMention === true,
|
|
349
|
+
hasAnyMention: explicitMention.hasAnyMention,
|
|
350
|
+
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
|
351
|
+
cfg: config,
|
|
352
|
+
surface: "zalouser",
|
|
353
|
+
}),
|
|
354
|
+
hasControlCommand,
|
|
355
|
+
commandAuthorized: commandAuthorized === true,
|
|
356
|
+
});
|
|
357
|
+
if (isGroup && mentionGate.shouldSkip) {
|
|
358
|
+
logVerbose(core, runtime, `zalouser: skip group ${chatId} (mention required, not mentioned)`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const fromLabel = isGroup ? groupName || `group:${chatId}` : senderName || `user:${senderId}`;
|
|
319
363
|
const storePath = core.channel.session.resolveStorePath(config.session?.store, {
|
|
320
364
|
agentId: route.agentId,
|
|
321
365
|
});
|
|
@@ -327,7 +371,7 @@ async function processMessage(
|
|
|
327
371
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
328
372
|
channel: "Zalo Personal",
|
|
329
373
|
from: fromLabel,
|
|
330
|
-
timestamp:
|
|
374
|
+
timestamp: message.timestampMs,
|
|
331
375
|
previousTimestamp,
|
|
332
376
|
envelope: envelopeOptions,
|
|
333
377
|
body: rawBody,
|
|
@@ -344,12 +388,24 @@ async function processMessage(
|
|
|
344
388
|
AccountId: route.accountId,
|
|
345
389
|
ChatType: isGroup ? "group" : "direct",
|
|
346
390
|
ConversationLabel: fromLabel,
|
|
391
|
+
GroupSubject: isGroup ? groupName || undefined : undefined,
|
|
392
|
+
GroupChannel: isGroup ? groupName || undefined : undefined,
|
|
393
|
+
GroupMembers: isGroup ? groupMembers : undefined,
|
|
347
394
|
SenderName: senderName || undefined,
|
|
348
395
|
SenderId: senderId,
|
|
396
|
+
WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : undefined,
|
|
349
397
|
CommandAuthorized: commandAuthorized,
|
|
350
398
|
Provider: "zalouser",
|
|
351
399
|
Surface: "zalouser",
|
|
352
|
-
MessageSid:
|
|
400
|
+
MessageSid: resolveZalouserMessageSid({
|
|
401
|
+
msgId: message.msgId,
|
|
402
|
+
cliMsgId: message.cliMsgId,
|
|
403
|
+
fallback: `${message.timestampMs}`,
|
|
404
|
+
}),
|
|
405
|
+
MessageSidFull: formatZalouserMessageSidFull({
|
|
406
|
+
msgId: message.msgId,
|
|
407
|
+
cliMsgId: message.cliMsgId,
|
|
408
|
+
}),
|
|
353
409
|
OriginatingChannel: "zalouser",
|
|
354
410
|
OriginatingTo: `zalouser:${chatId}`,
|
|
355
411
|
});
|
|
@@ -369,12 +425,24 @@ async function processMessage(
|
|
|
369
425
|
channel: "zalouser",
|
|
370
426
|
accountId: account.accountId,
|
|
371
427
|
});
|
|
428
|
+
const typingCallbacks = createTypingCallbacks({
|
|
429
|
+
start: async () => {
|
|
430
|
+
await sendTypingZalouser(chatId, {
|
|
431
|
+
profile: account.profile,
|
|
432
|
+
isGroup,
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
onStartError: (err) => {
|
|
436
|
+
logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
|
|
437
|
+
},
|
|
438
|
+
});
|
|
372
439
|
|
|
373
440
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
374
441
|
ctx: ctxPayload,
|
|
375
442
|
cfg: config,
|
|
376
443
|
dispatcherOptions: {
|
|
377
444
|
...prefixOptions,
|
|
445
|
+
typingCallbacks,
|
|
378
446
|
deliver: async (payload) => {
|
|
379
447
|
await deliverZalouserReply({
|
|
380
448
|
payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
|
|
@@ -466,10 +534,6 @@ export async function monitorZalouserProvider(
|
|
|
466
534
|
const { abortSignal, statusSink, runtime } = options;
|
|
467
535
|
|
|
468
536
|
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
537
|
|
|
474
538
|
try {
|
|
475
539
|
const profile = account.profile;
|
|
@@ -478,147 +542,144 @@ export async function monitorZalouserProvider(
|
|
|
478
542
|
.filter((entry) => entry && entry !== "*");
|
|
479
543
|
|
|
480
544
|
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
|
-
}
|
|
545
|
+
const friends = await listZaloFriends(profile);
|
|
546
|
+
const byName = buildNameIndex(friends, (friend) => friend.displayName);
|
|
547
|
+
const additions: string[] = [];
|
|
548
|
+
const mapping: string[] = [];
|
|
549
|
+
const unresolved: string[] = [];
|
|
550
|
+
for (const entry of allowFromEntries) {
|
|
551
|
+
if (/^\d+$/.test(entry)) {
|
|
552
|
+
additions.push(entry);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const matches = byName.get(entry.toLowerCase()) ?? [];
|
|
556
|
+
const match = matches[0];
|
|
557
|
+
const id = match?.userId ? String(match.userId) : undefined;
|
|
558
|
+
if (id) {
|
|
559
|
+
additions.push(id);
|
|
560
|
+
mapping.push(`${entry}→${id}`);
|
|
561
|
+
} else {
|
|
562
|
+
unresolved.push(entry);
|
|
502
563
|
}
|
|
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
564
|
}
|
|
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);
|
|
515
574
|
}
|
|
516
575
|
|
|
517
576
|
const groupsConfig = account.config.groups ?? {};
|
|
518
577
|
const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
|
|
519
578
|
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;
|
|
579
|
+
const groups = await listZaloGroups(profile);
|
|
580
|
+
const byName = buildNameIndex(groups, (group) => group.name);
|
|
581
|
+
const mapping: string[] = [];
|
|
582
|
+
const unresolved: string[] = [];
|
|
583
|
+
const nextGroups = { ...groupsConfig };
|
|
584
|
+
for (const entry of groupKeys) {
|
|
585
|
+
const cleaned = normalizeZalouserEntry(entry);
|
|
586
|
+
if (/^\d+$/.test(cleaned)) {
|
|
587
|
+
if (!nextGroups[cleaned]) {
|
|
588
|
+
nextGroups[cleaned] = groupsConfig[entry];
|
|
535
589
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
unresolved.push(entry);
|
|
590
|
+
mapping.push(`${entry}→${cleaned}`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const matches = byName.get(cleaned.toLowerCase()) ?? [];
|
|
594
|
+
const match = matches[0];
|
|
595
|
+
const id = match?.groupId ? String(match.groupId) : undefined;
|
|
596
|
+
if (id) {
|
|
597
|
+
if (!nextGroups[id]) {
|
|
598
|
+
nextGroups[id] = groupsConfig[entry];
|
|
546
599
|
}
|
|
600
|
+
mapping.push(`${entry}→${id}`);
|
|
601
|
+
} else {
|
|
602
|
+
unresolved.push(entry);
|
|
547
603
|
}
|
|
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
604
|
}
|
|
605
|
+
account = {
|
|
606
|
+
...account,
|
|
607
|
+
config: {
|
|
608
|
+
...account.config,
|
|
609
|
+
groups: nextGroups,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
summarizeMapping("zalouser groups", mapping, unresolved, runtime);
|
|
559
613
|
}
|
|
560
614
|
} catch (err) {
|
|
561
615
|
runtime.log?.(`zalouser resolve failed; using config entries. ${String(err)}`);
|
|
562
616
|
}
|
|
563
617
|
|
|
618
|
+
let listenerStop: (() => void) | null = null;
|
|
619
|
+
let stopped = false;
|
|
620
|
+
|
|
564
621
|
const stop = () => {
|
|
565
|
-
stopped
|
|
566
|
-
|
|
567
|
-
clearTimeout(restartTimer);
|
|
568
|
-
restartTimer = null;
|
|
569
|
-
}
|
|
570
|
-
if (proc) {
|
|
571
|
-
proc.kill("SIGTERM");
|
|
572
|
-
proc = null;
|
|
622
|
+
if (stopped) {
|
|
623
|
+
return;
|
|
573
624
|
}
|
|
574
|
-
|
|
625
|
+
stopped = true;
|
|
626
|
+
listenerStop?.();
|
|
627
|
+
listenerStop = null;
|
|
575
628
|
};
|
|
576
629
|
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
630
|
+
const listener = await startZaloListener({
|
|
631
|
+
accountId: account.accountId,
|
|
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
|
+
});
|
|
582
651
|
|
|
583
|
-
|
|
584
|
-
core,
|
|
585
|
-
runtime,
|
|
586
|
-
`[${account.accountId}] starting zca listener (profile=${account.profile})`,
|
|
587
|
-
);
|
|
652
|
+
listenerStop = listener.stop;
|
|
588
653
|
|
|
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
|
-
});
|
|
598
|
-
},
|
|
599
|
-
(err) => {
|
|
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
|
-
}
|
|
654
|
+
await new Promise<void>((resolve) => {
|
|
655
|
+
abortSignal.addEventListener(
|
|
656
|
+
"abort",
|
|
657
|
+
() => {
|
|
658
|
+
stop();
|
|
659
|
+
resolve();
|
|
607
660
|
},
|
|
608
|
-
|
|
661
|
+
{ once: true },
|
|
609
662
|
);
|
|
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
663
|
});
|
|
617
664
|
|
|
618
|
-
startListener();
|
|
619
|
-
|
|
620
|
-
// Wait for the running promise to resolve (on abort/stop)
|
|
621
|
-
await runningPromise;
|
|
622
|
-
|
|
623
665
|
return { stop };
|
|
624
666
|
}
|
|
667
|
+
|
|
668
|
+
export const __testing = {
|
|
669
|
+
processMessage: async (params: {
|
|
670
|
+
message: ZaloInboundMessage;
|
|
671
|
+
account: ResolvedZalouserAccount;
|
|
672
|
+
config: OpenClawConfig;
|
|
673
|
+
runtime: RuntimeEnv;
|
|
674
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
675
|
+
}) => {
|
|
676
|
+
await processMessage(
|
|
677
|
+
params.message,
|
|
678
|
+
params.account,
|
|
679
|
+
params.config,
|
|
680
|
+
getZalouserRuntime(),
|
|
681
|
+
params.runtime,
|
|
682
|
+
params.statusSink,
|
|
683
|
+
);
|
|
684
|
+
},
|
|
685
|
+
};
|