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