@openclaw/zalouser 2026.3.12 → 2026.5.1-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/README.md +4 -3
- package/api.ts +9 -0
- package/channel-plugin-api.ts +3 -0
- package/contract-api.ts +2 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +29 -24
- package/openclaw.plugin.json +288 -1
- package/package.json +38 -11
- package/runtime-api.ts +67 -0
- package/secret-contract-api.ts +4 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +2 -0
- package/src/accounts.runtime.ts +1 -0
- package/src/accounts.test-mocks.ts +14 -0
- package/src/accounts.test.ts +53 -1
- package/src/accounts.ts +52 -37
- package/src/channel-api.ts +20 -0
- package/src/channel.adapters.ts +390 -0
- package/src/channel.directory.test.ts +48 -61
- package/src/channel.runtime.ts +12 -0
- package/src/channel.sendpayload.test.ts +42 -37
- package/src/channel.setup.test.ts +33 -0
- package/src/channel.setup.ts +12 -0
- package/src/channel.test.ts +258 -56
- package/src/channel.ts +176 -692
- package/src/config-schema.ts +5 -5
- package/src/directory.ts +54 -0
- package/src/doctor-contract.ts +156 -0
- package/src/doctor.test.ts +77 -0
- package/src/doctor.ts +37 -0
- package/src/group-policy.test.ts +4 -4
- package/src/group-policy.ts +4 -2
- package/src/monitor.account-scope.test.ts +4 -10
- package/src/monitor.group-gating.test.ts +319 -190
- package/src/monitor.ts +233 -182
- package/src/probe.ts +3 -2
- package/src/qr-temp-file.ts +1 -1
- package/src/reaction.ts +5 -2
- package/src/runtime.ts +6 -3
- package/src/security-audit.test.ts +80 -0
- package/src/security-audit.ts +71 -0
- package/src/send.test.ts +2 -2
- package/src/send.ts +3 -3
- package/src/session-route.ts +121 -0
- package/src/setup-core.ts +33 -0
- package/src/setup-surface.test.ts +363 -0
- package/src/setup-surface.ts +470 -0
- package/src/setup-test-helpers.ts +42 -0
- package/src/shared.ts +92 -0
- package/src/status-issues.test.ts +5 -17
- package/src/status-issues.ts +18 -30
- package/src/test-helpers.ts +26 -0
- package/src/text-styles.test.ts +1 -1
- package/src/text-styles.ts +5 -2
- package/src/tool.test.ts +66 -3
- package/src/tool.ts +76 -14
- package/src/types.ts +3 -3
- package/src/zalo-js.credentials.test.ts +465 -0
- package/src/zalo-js.test-mocks.ts +89 -0
- package/src/zalo-js.ts +491 -274
- package/src/zca-client.test.ts +24 -0
- package/src/zca-client.ts +24 -58
- package/src/zca-constants.ts +55 -0
- package/test-api.ts +21 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/onboarding.ts +0 -340
package/src/monitor.ts
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
|
+
import { mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
|
|
2
|
+
import {
|
|
3
|
+
implicitMentionKindWhen,
|
|
4
|
+
resolveInboundMentionDecision,
|
|
5
|
+
} from "openclaw/plugin-sdk/channel-inbound";
|
|
6
|
+
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
1
7
|
import {
|
|
2
8
|
DM_GROUP_ACCESS_REASON,
|
|
9
|
+
resolveDmGroupAccessWithLists,
|
|
10
|
+
} from "openclaw/plugin-sdk/channel-policy";
|
|
11
|
+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
12
|
+
import { resolveSenderCommandAuthorization } from "openclaw/plugin-sdk/command-auth";
|
|
13
|
+
import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
14
|
+
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/core";
|
|
15
|
+
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
|
16
|
+
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
17
|
+
import {
|
|
18
|
+
evaluateGroupRouteAccessForPolicy,
|
|
19
|
+
resolveSenderScopedGroupPolicy,
|
|
20
|
+
} from "openclaw/plugin-sdk/group-access";
|
|
21
|
+
import {
|
|
3
22
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
4
23
|
type HistoryEntry,
|
|
5
|
-
KeyedAsyncQueue,
|
|
6
24
|
buildPendingHistoryContextFromMap,
|
|
7
25
|
clearHistoryEntriesIfEnabled,
|
|
8
26
|
recordPendingHistoryEntryIfEnabled,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "openclaw/plugin-sdk/zalouser";
|
|
27
|
+
} from "openclaw/plugin-sdk/reply-history";
|
|
28
|
+
import {
|
|
29
|
+
deliverTextOrMediaReply,
|
|
30
|
+
resolveSendableOutboundReplyParts,
|
|
31
|
+
type OutboundReplyPayload,
|
|
32
|
+
} from "openclaw/plugin-sdk/reply-payload";
|
|
33
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
|
17
34
|
import {
|
|
18
|
-
createTypingCallbacks,
|
|
19
|
-
createScopedPairingAccess,
|
|
20
|
-
createReplyPrefixOptions,
|
|
21
|
-
evaluateGroupRouteAccessForPolicy,
|
|
22
|
-
isDangerousNameMatchingEnabled,
|
|
23
|
-
issuePairingChallenge,
|
|
24
|
-
resolveOutboundMediaUrls,
|
|
25
|
-
mergeAllowlist,
|
|
26
|
-
resolveMentionGatingWithBypass,
|
|
27
|
-
resolveOpenProviderRuntimeGroupPolicy,
|
|
28
35
|
resolveDefaultGroupPolicy,
|
|
29
|
-
|
|
30
|
-
sendMediaWithLeadingCaption,
|
|
31
|
-
summarizeMapping,
|
|
36
|
+
resolveOpenProviderRuntimeGroupPolicy,
|
|
32
37
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
33
|
-
} from "openclaw/plugin-sdk/
|
|
38
|
+
} from "openclaw/plugin-sdk/runtime-group-policy";
|
|
39
|
+
import {
|
|
40
|
+
normalizeLowercaseStringOrEmpty,
|
|
41
|
+
normalizeOptionalLowercaseString,
|
|
42
|
+
} from "openclaw/plugin-sdk/text-runtime";
|
|
34
43
|
import {
|
|
35
44
|
buildZalouserGroupCandidates,
|
|
36
45
|
findZalouserGroupEntry,
|
|
@@ -73,7 +82,7 @@ function normalizeZalouserEntry(entry: string): string {
|
|
|
73
82
|
function buildNameIndex<T>(items: T[], nameFn: (item: T) => string | undefined): Map<string, T[]> {
|
|
74
83
|
const index = new Map<string, T[]>();
|
|
75
84
|
for (const item of items) {
|
|
76
|
-
const name = nameFn(item)
|
|
85
|
+
const name = normalizeOptionalLowercaseString(nameFn(item));
|
|
77
86
|
if (!name) {
|
|
78
87
|
continue;
|
|
79
88
|
}
|
|
@@ -100,9 +109,9 @@ function resolveUserAllowlistEntries(
|
|
|
100
109
|
additions.push(entry);
|
|
101
110
|
continue;
|
|
102
111
|
}
|
|
103
|
-
const matches = byName.get(entry
|
|
112
|
+
const matches = byName.get(normalizeLowercaseStringOrEmpty(entry)) ?? [];
|
|
104
113
|
const match = matches[0];
|
|
105
|
-
const id = match?.userId
|
|
114
|
+
const id = match?.userId;
|
|
106
115
|
if (id) {
|
|
107
116
|
additions.push(id);
|
|
108
117
|
mapping.push(`${entry}->${id}`);
|
|
@@ -129,16 +138,6 @@ function resolveInboundQueueKey(message: ZaloInboundMessage): string {
|
|
|
129
138
|
return `direct:${senderId || threadId}`;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
function createDeferred<T>() {
|
|
133
|
-
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
134
|
-
let reject!: (reason?: unknown) => void;
|
|
135
|
-
const promise = new Promise<T>((res, rej) => {
|
|
136
|
-
resolve = res;
|
|
137
|
-
reject = rej;
|
|
138
|
-
});
|
|
139
|
-
return { promise, resolve, reject };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
141
|
function resolveZalouserDmSessionScope(config: OpenClawConfig) {
|
|
143
142
|
const configured = config.session?.dmScope;
|
|
144
143
|
return configured === "main" || !configured ? "per-channel-peer" : configured;
|
|
@@ -156,24 +155,24 @@ function resolveZalouserInboundSessionKey(params: {
|
|
|
156
155
|
return params.route.sessionKey;
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
const directSessionKey =
|
|
160
|
-
.buildAgentSessionKey({
|
|
158
|
+
const directSessionKey = normalizeLowercaseStringOrEmpty(
|
|
159
|
+
params.core.channel.routing.buildAgentSessionKey({
|
|
161
160
|
agentId: params.route.agentId,
|
|
162
161
|
channel: "zalouser",
|
|
163
162
|
accountId: params.route.accountId,
|
|
164
163
|
peer: { kind: "direct", id: params.senderId },
|
|
165
164
|
dmScope: resolveZalouserDmSessionScope(params.config),
|
|
166
165
|
identityLinks: params.config.session?.identityLinks,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
const legacySessionKey =
|
|
170
|
-
.buildAgentSessionKey({
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
const legacySessionKey = normalizeLowercaseStringOrEmpty(
|
|
169
|
+
params.core.channel.routing.buildAgentSessionKey({
|
|
171
170
|
agentId: params.route.agentId,
|
|
172
171
|
channel: "zalouser",
|
|
173
172
|
accountId: params.route.accountId,
|
|
174
173
|
peer: { kind: "group", id: params.senderId },
|
|
175
|
-
})
|
|
176
|
-
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
177
176
|
const hasDirectSession =
|
|
178
177
|
params.core.channel.session.readSessionUpdatedAt({
|
|
179
178
|
storePath: params.storePath,
|
|
@@ -199,12 +198,12 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
|
|
|
199
198
|
if (allowFrom.includes("*")) {
|
|
200
199
|
return true;
|
|
201
200
|
}
|
|
202
|
-
const normalizedSenderId = senderId
|
|
201
|
+
const normalizedSenderId = normalizeOptionalLowercaseString(senderId);
|
|
203
202
|
if (!normalizedSenderId) {
|
|
204
203
|
return false;
|
|
205
204
|
}
|
|
206
205
|
return allowFrom.some((entry) => {
|
|
207
|
-
const normalized = entry
|
|
206
|
+
const normalized = normalizeLowercaseStringOrEmpty(entry).replace(/^(zalouser|zlu):/i, "");
|
|
208
207
|
return normalized === normalizedSenderId;
|
|
209
208
|
});
|
|
210
209
|
}
|
|
@@ -212,7 +211,7 @@ function isSenderAllowed(senderId: string | undefined, allowFrom: string[]): boo
|
|
|
212
211
|
function resolveGroupRequireMention(params: {
|
|
213
212
|
groupId: string;
|
|
214
213
|
groupName?: string | null;
|
|
215
|
-
groups: Record<string, {
|
|
214
|
+
groups: Record<string, { enabled?: boolean; requireMention?: boolean }>;
|
|
216
215
|
allowNameMatching?: boolean;
|
|
217
216
|
}): boolean {
|
|
218
217
|
const entry = findZalouserGroupEntry(
|
|
@@ -258,7 +257,7 @@ async function processMessage(
|
|
|
258
257
|
historyState: ZalouserGroupHistoryState,
|
|
259
258
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
|
260
259
|
): Promise<void> {
|
|
261
|
-
const pairing =
|
|
260
|
+
const pairing = createChannelPairingController({
|
|
262
261
|
core,
|
|
263
262
|
channel: "zalouser",
|
|
264
263
|
accountId: account.accountId,
|
|
@@ -319,6 +318,7 @@ async function processMessage(
|
|
|
319
318
|
});
|
|
320
319
|
|
|
321
320
|
const groups = account.config.groups ?? {};
|
|
321
|
+
const routeAllowlistConfigured = Object.keys(groups).length > 0;
|
|
322
322
|
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
323
323
|
if (isGroup) {
|
|
324
324
|
const groupEntry = findZalouserGroupEntry(
|
|
@@ -333,7 +333,7 @@ async function processMessage(
|
|
|
333
333
|
);
|
|
334
334
|
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
|
335
335
|
groupPolicy,
|
|
336
|
-
routeAllowlistConfigured
|
|
336
|
+
routeAllowlistConfigured,
|
|
337
337
|
routeMatched: Boolean(groupEntry),
|
|
338
338
|
routeEnabled: isZalouserGroupEntryAllowed(groupEntry),
|
|
339
339
|
});
|
|
@@ -358,21 +358,25 @@ async function processMessage(
|
|
|
358
358
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
359
359
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
360
360
|
const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((v) => String(v));
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
const senderGroupPolicy =
|
|
362
|
+
routeAllowlistConfigured && configGroupAllowFrom.length === 0
|
|
363
|
+
? groupPolicy
|
|
364
|
+
: resolveSenderScopedGroupPolicy({
|
|
365
|
+
groupPolicy,
|
|
366
|
+
groupAllowFrom: configGroupAllowFrom,
|
|
367
|
+
});
|
|
365
368
|
const storeAllowFrom =
|
|
366
|
-
!isGroup && dmPolicy !== "allowlist" &&
|
|
369
|
+
!isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open"
|
|
367
370
|
? await pairing.readAllowFromStore().catch(() => [])
|
|
368
371
|
: [];
|
|
369
372
|
const accessDecision = resolveDmGroupAccessWithLists({
|
|
370
373
|
isGroup,
|
|
371
374
|
dmPolicy,
|
|
372
|
-
groupPolicy,
|
|
375
|
+
groupPolicy: senderGroupPolicy,
|
|
373
376
|
allowFrom: configAllowFrom,
|
|
374
377
|
groupAllowFrom: configGroupAllowFrom,
|
|
375
378
|
storeAllowFrom,
|
|
379
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
376
380
|
isSenderAllowed: (allowFrom) => isSenderAllowed(senderId, allowFrom),
|
|
377
381
|
});
|
|
378
382
|
if (isGroup && accessDecision.decision !== "allow") {
|
|
@@ -390,12 +394,10 @@ async function processMessage(
|
|
|
390
394
|
|
|
391
395
|
if (!isGroup && accessDecision.decision !== "allow") {
|
|
392
396
|
if (accessDecision.decision === "pairing") {
|
|
393
|
-
await
|
|
394
|
-
channel: "zalouser",
|
|
397
|
+
await pairing.issueChallenge({
|
|
395
398
|
senderId,
|
|
396
399
|
senderIdLine: `Your Zalo user id: ${senderId}`,
|
|
397
400
|
meta: { name: senderName || undefined },
|
|
398
|
-
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
399
401
|
onCreated: () => {
|
|
400
402
|
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
|
|
401
403
|
},
|
|
@@ -434,6 +436,8 @@ async function processMessage(
|
|
|
434
436
|
configuredGroupAllowFrom: configGroupAllowFrom,
|
|
435
437
|
senderId,
|
|
436
438
|
isSenderAllowed,
|
|
439
|
+
channel: "zalouser",
|
|
440
|
+
accountId: account.accountId,
|
|
437
441
|
readAllowFromStore: async () => storeAllowFrom,
|
|
438
442
|
shouldComputeCommandAuthorized: (body, cfg) =>
|
|
439
443
|
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
|
@@ -488,28 +492,32 @@ async function processMessage(
|
|
|
488
492
|
})
|
|
489
493
|
: true;
|
|
490
494
|
const canDetectMention = mentionRegexes.length > 0 || explicitMention.canResolveExplicit;
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
495
|
+
const mentionDecision = resolveInboundMentionDecision({
|
|
496
|
+
facts: {
|
|
497
|
+
canDetectMention,
|
|
498
|
+
wasMentioned,
|
|
499
|
+
hasAnyMention: explicitMention.hasAnyMention,
|
|
500
|
+
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", message.implicitMention === true),
|
|
501
|
+
},
|
|
502
|
+
policy: {
|
|
503
|
+
isGroup,
|
|
504
|
+
requireMention,
|
|
505
|
+
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
|
506
|
+
cfg: config,
|
|
507
|
+
surface: "zalouser",
|
|
508
|
+
}),
|
|
509
|
+
hasControlCommand,
|
|
510
|
+
commandAuthorized: commandAuthorized === true,
|
|
511
|
+
},
|
|
504
512
|
});
|
|
505
|
-
if (isGroup && requireMention && !canDetectMention && !
|
|
513
|
+
if (isGroup && requireMention && !canDetectMention && !mentionDecision.effectiveWasMentioned) {
|
|
506
514
|
runtime.error?.(
|
|
507
515
|
`[${account.accountId}] zalouser mention required but detection unavailable ` +
|
|
508
516
|
`(missing mention regexes and bot self id); dropping group ${chatId}`,
|
|
509
517
|
);
|
|
510
518
|
return;
|
|
511
519
|
}
|
|
512
|
-
if (isGroup &&
|
|
520
|
+
if (isGroup && mentionDecision.shouldSkip) {
|
|
513
521
|
recordPendingHistoryEntryIfEnabled({
|
|
514
522
|
historyMap: historyState.groupHistories,
|
|
515
523
|
historyKey: historyKey ?? "",
|
|
@@ -586,102 +594,144 @@ async function processMessage(
|
|
|
586
594
|
: undefined;
|
|
587
595
|
|
|
588
596
|
const normalizedTo = isGroup ? `zalouser:group:${chatId}` : `zalouser:${chatId}`;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
InboundHistory: inboundHistory,
|
|
594
|
-
RawBody: rawBody,
|
|
595
|
-
CommandBody: commandBody,
|
|
596
|
-
BodyForCommands: commandBody,
|
|
597
|
-
From: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
|
|
598
|
-
To: normalizedTo,
|
|
599
|
-
SessionKey: inboundSessionKey,
|
|
600
|
-
AccountId: route.accountId,
|
|
601
|
-
ChatType: isGroup ? "group" : "direct",
|
|
602
|
-
ConversationLabel: fromLabel,
|
|
603
|
-
GroupSubject: isGroup ? groupName || undefined : undefined,
|
|
604
|
-
GroupChannel: isGroup ? groupName || undefined : undefined,
|
|
605
|
-
GroupMembers: isGroup ? groupMembers : undefined,
|
|
606
|
-
SenderName: senderName || undefined,
|
|
607
|
-
SenderId: senderId,
|
|
608
|
-
WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : undefined,
|
|
609
|
-
CommandAuthorized: commandAuthorized,
|
|
610
|
-
Provider: "zalouser",
|
|
611
|
-
Surface: "zalouser",
|
|
612
|
-
MessageSid: resolveZalouserMessageSid({
|
|
613
|
-
msgId: message.msgId,
|
|
614
|
-
cliMsgId: message.cliMsgId,
|
|
615
|
-
fallback: `${message.timestampMs}`,
|
|
616
|
-
}),
|
|
617
|
-
MessageSidFull: formatZalouserMessageSidFull({
|
|
618
|
-
msgId: message.msgId,
|
|
619
|
-
cliMsgId: message.cliMsgId,
|
|
620
|
-
}),
|
|
621
|
-
OriginatingChannel: "zalouser",
|
|
622
|
-
OriginatingTo: normalizedTo,
|
|
597
|
+
const messageSid = resolveZalouserMessageSid({
|
|
598
|
+
msgId: message.msgId,
|
|
599
|
+
cliMsgId: message.cliMsgId,
|
|
600
|
+
fallback: `${message.timestampMs}`,
|
|
623
601
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
628
|
-
ctx: ctxPayload,
|
|
629
|
-
onRecordError: (err) => {
|
|
630
|
-
runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
|
|
631
|
-
},
|
|
602
|
+
const messageSidFull = formatZalouserMessageSidFull({
|
|
603
|
+
msgId: message.msgId,
|
|
604
|
+
cliMsgId: message.cliMsgId,
|
|
632
605
|
});
|
|
633
606
|
|
|
634
|
-
const
|
|
635
|
-
cfg: config,
|
|
636
|
-
agentId: route.agentId,
|
|
607
|
+
const ctxPayload = core.channel.turn.buildContext({
|
|
637
608
|
channel: "zalouser",
|
|
638
|
-
accountId:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
609
|
+
accountId: route.accountId,
|
|
610
|
+
messageId: messageSid,
|
|
611
|
+
messageIdFull: messageSidFull,
|
|
612
|
+
timestamp: message.timestampMs,
|
|
613
|
+
from: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
|
|
614
|
+
sender: {
|
|
615
|
+
id: senderId,
|
|
616
|
+
name: senderName || undefined,
|
|
646
617
|
},
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
618
|
+
conversation: {
|
|
619
|
+
kind: isGroup ? "group" : "direct",
|
|
620
|
+
id: chatId,
|
|
621
|
+
label: fromLabel,
|
|
622
|
+
routePeer: {
|
|
623
|
+
kind: isGroup ? "group" : "direct",
|
|
624
|
+
id: chatId,
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
route: {
|
|
628
|
+
agentId: route.agentId,
|
|
629
|
+
accountId: route.accountId,
|
|
630
|
+
routeSessionKey: route.sessionKey,
|
|
631
|
+
dispatchSessionKey: inboundSessionKey,
|
|
632
|
+
},
|
|
633
|
+
reply: {
|
|
634
|
+
to: normalizedTo,
|
|
635
|
+
originatingTo: normalizedTo,
|
|
636
|
+
},
|
|
637
|
+
message: {
|
|
638
|
+
body: combinedBody,
|
|
639
|
+
bodyForAgent: rawBody,
|
|
640
|
+
rawBody,
|
|
641
|
+
commandBody,
|
|
642
|
+
inboundHistory,
|
|
643
|
+
envelopeFrom: fromLabel,
|
|
644
|
+
},
|
|
645
|
+
extra: {
|
|
646
|
+
BodyForCommands: commandBody,
|
|
647
|
+
GroupSubject: isGroup ? groupName || undefined : undefined,
|
|
648
|
+
GroupChannel: isGroup ? groupName || undefined : undefined,
|
|
649
|
+
GroupMembers: isGroup ? groupMembers : undefined,
|
|
650
|
+
WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : undefined,
|
|
651
|
+
CommandAuthorized: commandAuthorized,
|
|
652
652
|
},
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
-
|
|
656
|
-
ctx: ctxPayload,
|
|
655
|
+
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
|
|
657
656
|
cfg: config,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
657
|
+
agentId: route.agentId,
|
|
658
|
+
channel: "zalouser",
|
|
659
|
+
accountId: account.accountId,
|
|
660
|
+
typing: {
|
|
661
|
+
start: async () => {
|
|
662
|
+
await sendTypingZalouser(chatId, {
|
|
664
663
|
profile: account.profile,
|
|
665
|
-
chatId,
|
|
666
664
|
isGroup,
|
|
667
|
-
runtime,
|
|
668
|
-
core,
|
|
669
|
-
config,
|
|
670
|
-
accountId: account.accountId,
|
|
671
|
-
statusSink,
|
|
672
|
-
tableMode: core.channel.text.resolveMarkdownTableMode({
|
|
673
|
-
cfg: config,
|
|
674
|
-
channel: "zalouser",
|
|
675
|
-
accountId: account.accountId,
|
|
676
|
-
}),
|
|
677
665
|
});
|
|
678
666
|
},
|
|
679
|
-
|
|
680
|
-
runtime.error(
|
|
667
|
+
onStartError: (err) => {
|
|
668
|
+
runtime.error?.(
|
|
669
|
+
`[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`,
|
|
670
|
+
);
|
|
671
|
+
logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
|
|
681
672
|
},
|
|
682
673
|
},
|
|
683
|
-
|
|
684
|
-
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
await core.channel.turn.run({
|
|
677
|
+
channel: "zalouser",
|
|
678
|
+
accountId: account.accountId,
|
|
679
|
+
raw: message,
|
|
680
|
+
adapter: {
|
|
681
|
+
ingest: () => ({
|
|
682
|
+
id: messageSid ?? `${message.timestampMs}`,
|
|
683
|
+
timestamp: message.timestampMs,
|
|
684
|
+
rawText: rawBody,
|
|
685
|
+
textForAgent: rawBody,
|
|
686
|
+
textForCommands: commandBody,
|
|
687
|
+
raw: message,
|
|
688
|
+
}),
|
|
689
|
+
resolveTurn: () => ({
|
|
690
|
+
cfg: config,
|
|
691
|
+
channel: "zalouser",
|
|
692
|
+
accountId: account.accountId,
|
|
693
|
+
agentId: route.agentId,
|
|
694
|
+
routeSessionKey: route.sessionKey,
|
|
695
|
+
storePath,
|
|
696
|
+
ctxPayload,
|
|
697
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
698
|
+
dispatchReplyWithBufferedBlockDispatcher:
|
|
699
|
+
core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
700
|
+
delivery: {
|
|
701
|
+
deliver: async (payload) => {
|
|
702
|
+
await deliverZalouserReply({
|
|
703
|
+
payload: payload as { text?: string; mediaUrls?: string[]; mediaUrl?: string },
|
|
704
|
+
profile: account.profile,
|
|
705
|
+
chatId,
|
|
706
|
+
isGroup,
|
|
707
|
+
runtime,
|
|
708
|
+
core,
|
|
709
|
+
config,
|
|
710
|
+
accountId: account.accountId,
|
|
711
|
+
statusSink,
|
|
712
|
+
tableMode: core.channel.text.resolveMarkdownTableMode({
|
|
713
|
+
cfg: config,
|
|
714
|
+
channel: "zalouser",
|
|
715
|
+
accountId: account.accountId,
|
|
716
|
+
}),
|
|
717
|
+
});
|
|
718
|
+
},
|
|
719
|
+
onError: (err, info) => {
|
|
720
|
+
runtime.error(
|
|
721
|
+
`[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`,
|
|
722
|
+
);
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
dispatcherOptions: replyPipeline,
|
|
726
|
+
replyOptions: {
|
|
727
|
+
onModelSelected,
|
|
728
|
+
},
|
|
729
|
+
record: {
|
|
730
|
+
onRecordError: (err) => {
|
|
731
|
+
runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
}),
|
|
685
735
|
},
|
|
686
736
|
});
|
|
687
737
|
if (isGroup && historyKey) {
|
|
@@ -708,16 +758,31 @@ async function deliverZalouserReply(params: {
|
|
|
708
758
|
const { payload, profile, chatId, isGroup, runtime, core, config, accountId, statusSink } =
|
|
709
759
|
params;
|
|
710
760
|
const tableMode = params.tableMode ?? "code";
|
|
711
|
-
const
|
|
761
|
+
const reply = resolveSendableOutboundReplyParts(payload, {
|
|
762
|
+
text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
|
|
763
|
+
});
|
|
712
764
|
const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
|
|
713
765
|
const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, {
|
|
714
766
|
fallbackLimit: ZALOUSER_TEXT_LIMIT,
|
|
715
767
|
});
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
768
|
+
await deliverTextOrMediaReply({
|
|
769
|
+
payload,
|
|
770
|
+
text: reply.text,
|
|
771
|
+
sendText: async (chunk) => {
|
|
772
|
+
try {
|
|
773
|
+
await sendMessageZalouser(chatId, chunk, {
|
|
774
|
+
profile,
|
|
775
|
+
isGroup,
|
|
776
|
+
textMode: "markdown",
|
|
777
|
+
textChunkMode: chunkMode,
|
|
778
|
+
textChunkLimit,
|
|
779
|
+
});
|
|
780
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
781
|
+
} catch (err) {
|
|
782
|
+
runtime.error(`Zalouser message send failed: ${String(err)}`);
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
sendMedia: async ({ mediaUrl, caption }) => {
|
|
721
786
|
logVerbose(core, runtime, `Sending media to ${chatId}`);
|
|
722
787
|
await sendMessageZalouser(chatId, caption ?? "", {
|
|
723
788
|
profile,
|
|
@@ -729,28 +794,14 @@ async function deliverZalouserReply(params: {
|
|
|
729
794
|
});
|
|
730
795
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
731
796
|
},
|
|
732
|
-
|
|
733
|
-
runtime.error(
|
|
797
|
+
onMediaError: (error) => {
|
|
798
|
+
runtime.error(
|
|
799
|
+
`Zalouser media send failed: ${
|
|
800
|
+
error instanceof Error ? error.message : JSON.stringify(error)
|
|
801
|
+
}`,
|
|
802
|
+
);
|
|
734
803
|
},
|
|
735
804
|
});
|
|
736
|
-
if (sentMedia) {
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (text) {
|
|
741
|
-
try {
|
|
742
|
-
await sendMessageZalouser(chatId, text, {
|
|
743
|
-
profile,
|
|
744
|
-
isGroup,
|
|
745
|
-
textMode: "markdown",
|
|
746
|
-
textChunkMode: chunkMode,
|
|
747
|
-
textChunkLimit,
|
|
748
|
-
});
|
|
749
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
750
|
-
} catch (err) {
|
|
751
|
-
runtime.error(`Zalouser message send failed: ${String(err)}`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
805
|
}
|
|
755
806
|
|
|
756
807
|
export async function monitorZalouserProvider(
|
|
@@ -833,9 +884,9 @@ export async function monitorZalouserProvider(
|
|
|
833
884
|
mapping.push(`${entry}→${cleaned}`);
|
|
834
885
|
continue;
|
|
835
886
|
}
|
|
836
|
-
const matches = byName.get(cleaned
|
|
887
|
+
const matches = byName.get(normalizeLowercaseStringOrEmpty(cleaned)) ?? [];
|
|
837
888
|
const match = matches[0];
|
|
838
|
-
const id = match?.groupId
|
|
889
|
+
const id = match?.groupId;
|
|
839
890
|
if (id) {
|
|
840
891
|
if (!nextGroups[id]) {
|
|
841
892
|
nextGroups[id] = groupsConfig[entry];
|
package/src/probe.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BaseProbeResult } from "openclaw/plugin-sdk/
|
|
1
|
+
import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
|
|
2
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
2
3
|
import type { ZcaUserInfo } from "./types.js";
|
|
3
4
|
import { getZaloUserInfo } from "./zalo-js.js";
|
|
4
5
|
|
|
@@ -28,7 +29,7 @@ export async function probeZalouser(
|
|
|
28
29
|
} catch (error) {
|
|
29
30
|
return {
|
|
30
31
|
ok: false,
|
|
31
|
-
error:
|
|
32
|
+
error: formatErrorMessage(error),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
}
|
package/src/qr-temp-file.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/
|
|
3
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
|
4
4
|
|
|
5
5
|
export async function writeQrDataUrlToTempFile(
|
|
6
6
|
qrDataUrl: string,
|
package/src/reaction.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
+
import { Reactions } from "./zca-constants.js";
|
|
2
3
|
|
|
3
4
|
const REACTION_ALIAS_MAP = new Map<string, string>([
|
|
4
5
|
["like", Reactions.LIKE],
|
|
@@ -24,6 +25,8 @@ export function normalizeZaloReactionIcon(raw: string): string {
|
|
|
24
25
|
return Reactions.LIKE;
|
|
25
26
|
}
|
|
26
27
|
return (
|
|
27
|
-
REACTION_ALIAS_MAP.get(trimmed
|
|
28
|
+
REACTION_ALIAS_MAP.get(normalizeLowercaseStringOrEmpty(trimmed)) ??
|
|
29
|
+
REACTION_ALIAS_MAP.get(trimmed) ??
|
|
30
|
+
trimmed
|
|
28
31
|
);
|
|
29
32
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
3
3
|
|
|
4
4
|
const { setRuntime: setZalouserRuntime, getRuntime: getZalouserRuntime } =
|
|
5
|
-
createPluginRuntimeStore<PluginRuntime>(
|
|
5
|
+
createPluginRuntimeStore<PluginRuntime>({
|
|
6
|
+
pluginId: "zalouser",
|
|
7
|
+
errorMessage: "Zalouser runtime not initialized",
|
|
8
|
+
});
|
|
6
9
|
export { getZalouserRuntime, setZalouserRuntime };
|