@openclaw/zalouser 2026.2.25 → 2026.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +41 -147
- package/index.ts +1 -3
- package/package.json +4 -3
- package/src/accounts.test.ts +214 -0
- package/src/accounts.ts +28 -17
- package/src/channel.sendpayload.test.ts +117 -0
- package/src/channel.test.ts +123 -1
- package/src/channel.ts +244 -191
- package/src/config-schema.ts +1 -0
- package/src/group-policy.test.ts +49 -0
- package/src/group-policy.ts +78 -0
- package/src/message-sid.test.ts +66 -0
- package/src/message-sid.ts +80 -0
- package/src/monitor.account-scope.test.ts +123 -0
- package/src/monitor.group-gating.test.ts +216 -0
- package/src/monitor.ts +299 -228
- package/src/onboarding.ts +110 -142
- package/src/probe.test.ts +60 -0
- package/src/probe.ts +19 -12
- package/src/reaction.test.ts +19 -0
- package/src/reaction.ts +29 -0
- package/src/send.test.ts +116 -115
- package/src/send.ts +63 -117
- package/src/status-issues.test.ts +1 -15
- package/src/status-issues.ts +7 -26
- package/src/tool.test.ts +149 -0
- package/src/tool.ts +36 -54
- package/src/types.ts +52 -42
- package/src/zalo-js.ts +1401 -0
- package/src/zca-client.ts +249 -0
- package/src/zca-js-exports.d.ts +22 -0
- package/src/zca.ts +0 -198
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ZalouserGroupConfig } from "./types.js";
|
|
2
|
+
|
|
3
|
+
type ZalouserGroups = Record<string, ZalouserGroupConfig>;
|
|
4
|
+
|
|
5
|
+
function toGroupCandidate(value?: string | null): string {
|
|
6
|
+
return value?.trim() ?? "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeZalouserGroupSlug(raw?: string | null): string {
|
|
10
|
+
const trimmed = raw?.trim().toLowerCase() ?? "";
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
return trimmed
|
|
15
|
+
.replace(/^#/, "")
|
|
16
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
17
|
+
.replace(/^-+|-+$/g, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildZalouserGroupCandidates(params: {
|
|
21
|
+
groupId?: string | null;
|
|
22
|
+
groupChannel?: string | null;
|
|
23
|
+
groupName?: string | null;
|
|
24
|
+
includeGroupIdAlias?: boolean;
|
|
25
|
+
includeWildcard?: boolean;
|
|
26
|
+
}): string[] {
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
const push = (value?: string | null) => {
|
|
30
|
+
const normalized = toGroupCandidate(value);
|
|
31
|
+
if (!normalized || seen.has(normalized)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
seen.add(normalized);
|
|
35
|
+
out.push(normalized);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const groupId = toGroupCandidate(params.groupId);
|
|
39
|
+
const groupChannel = toGroupCandidate(params.groupChannel);
|
|
40
|
+
const groupName = toGroupCandidate(params.groupName);
|
|
41
|
+
|
|
42
|
+
push(groupId);
|
|
43
|
+
if (params.includeGroupIdAlias === true && groupId) {
|
|
44
|
+
push(`group:${groupId}`);
|
|
45
|
+
}
|
|
46
|
+
push(groupChannel);
|
|
47
|
+
push(groupName);
|
|
48
|
+
if (groupName) {
|
|
49
|
+
push(normalizeZalouserGroupSlug(groupName));
|
|
50
|
+
}
|
|
51
|
+
if (params.includeWildcard !== false) {
|
|
52
|
+
push("*");
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function findZalouserGroupEntry(
|
|
58
|
+
groups: ZalouserGroups | undefined,
|
|
59
|
+
candidates: string[],
|
|
60
|
+
): ZalouserGroupConfig | undefined {
|
|
61
|
+
if (!groups) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
const entry = groups[candidate];
|
|
66
|
+
if (entry) {
|
|
67
|
+
return entry;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isZalouserGroupEntryAllowed(entry: ZalouserGroupConfig | undefined): boolean {
|
|
74
|
+
if (!entry) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return entry.allow !== false && entry.enabled !== false;
|
|
78
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatZalouserMessageSidFull,
|
|
4
|
+
parseZalouserMessageSidFull,
|
|
5
|
+
resolveZalouserMessageSid,
|
|
6
|
+
resolveZalouserReactionMessageIds,
|
|
7
|
+
} from "./message-sid.js";
|
|
8
|
+
|
|
9
|
+
describe("zalouser message sid helpers", () => {
|
|
10
|
+
it("parses MessageSidFull pairs", () => {
|
|
11
|
+
expect(parseZalouserMessageSidFull("111:222")).toEqual({
|
|
12
|
+
msgId: "111",
|
|
13
|
+
cliMsgId: "222",
|
|
14
|
+
});
|
|
15
|
+
expect(parseZalouserMessageSidFull("111")).toBeNull();
|
|
16
|
+
expect(parseZalouserMessageSidFull(undefined)).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("resolves reaction ids from explicit params first", () => {
|
|
20
|
+
expect(
|
|
21
|
+
resolveZalouserReactionMessageIds({
|
|
22
|
+
messageId: "m-1",
|
|
23
|
+
cliMsgId: "c-1",
|
|
24
|
+
currentMessageId: "x:y",
|
|
25
|
+
}),
|
|
26
|
+
).toEqual({
|
|
27
|
+
msgId: "m-1",
|
|
28
|
+
cliMsgId: "c-1",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("resolves reaction ids from current message sid full", () => {
|
|
33
|
+
expect(
|
|
34
|
+
resolveZalouserReactionMessageIds({
|
|
35
|
+
currentMessageId: "m-2:c-2",
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({
|
|
38
|
+
msgId: "m-2",
|
|
39
|
+
cliMsgId: "c-2",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("falls back to duplicated current id when no pair is available", () => {
|
|
44
|
+
expect(
|
|
45
|
+
resolveZalouserReactionMessageIds({
|
|
46
|
+
currentMessageId: "solo",
|
|
47
|
+
}),
|
|
48
|
+
).toEqual({
|
|
49
|
+
msgId: "solo",
|
|
50
|
+
cliMsgId: "solo",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("formats message sid fields for context payload", () => {
|
|
55
|
+
expect(formatZalouserMessageSidFull({ msgId: "1", cliMsgId: "2" })).toBe("1:2");
|
|
56
|
+
expect(formatZalouserMessageSidFull({ msgId: "1" })).toBe("1");
|
|
57
|
+
expect(formatZalouserMessageSidFull({ cliMsgId: "2" })).toBe("2");
|
|
58
|
+
expect(formatZalouserMessageSidFull({})).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("resolves primary message sid with fallback timestamp", () => {
|
|
62
|
+
expect(resolveZalouserMessageSid({ msgId: "1", cliMsgId: "2", fallback: "t" })).toBe("1");
|
|
63
|
+
expect(resolveZalouserMessageSid({ cliMsgId: "2", fallback: "t" })).toBe("2");
|
|
64
|
+
expect(resolveZalouserMessageSid({ fallback: "t" })).toBe("t");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
function toMessageSidPart(value?: string | number | null): string {
|
|
2
|
+
if (typeof value === "string") {
|
|
3
|
+
return value.trim();
|
|
4
|
+
}
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6
|
+
return String(Math.trunc(value));
|
|
7
|
+
}
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseZalouserMessageSidFull(
|
|
12
|
+
value?: string | number | null,
|
|
13
|
+
): { msgId: string; cliMsgId: string } | null {
|
|
14
|
+
const raw = toMessageSidPart(value);
|
|
15
|
+
if (!raw) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const [msgIdPart, cliMsgIdPart] = raw.split(":").map((entry) => entry.trim());
|
|
19
|
+
if (!msgIdPart || !cliMsgIdPart) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return { msgId: msgIdPart, cliMsgId: cliMsgIdPart };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveZalouserReactionMessageIds(params: {
|
|
26
|
+
messageId?: string;
|
|
27
|
+
cliMsgId?: string;
|
|
28
|
+
currentMessageId?: string | number;
|
|
29
|
+
}): { msgId: string; cliMsgId: string } | null {
|
|
30
|
+
const explicitMessageId = toMessageSidPart(params.messageId);
|
|
31
|
+
const explicitCliMsgId = toMessageSidPart(params.cliMsgId);
|
|
32
|
+
if (explicitMessageId && explicitCliMsgId) {
|
|
33
|
+
return { msgId: explicitMessageId, cliMsgId: explicitCliMsgId };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parsedFromCurrent = parseZalouserMessageSidFull(params.currentMessageId);
|
|
37
|
+
if (parsedFromCurrent) {
|
|
38
|
+
return parsedFromCurrent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const currentRaw = toMessageSidPart(params.currentMessageId);
|
|
42
|
+
if (!currentRaw) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (explicitMessageId && !explicitCliMsgId) {
|
|
46
|
+
return { msgId: explicitMessageId, cliMsgId: currentRaw };
|
|
47
|
+
}
|
|
48
|
+
if (!explicitMessageId && explicitCliMsgId) {
|
|
49
|
+
return { msgId: currentRaw, cliMsgId: explicitCliMsgId };
|
|
50
|
+
}
|
|
51
|
+
return { msgId: currentRaw, cliMsgId: currentRaw };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatZalouserMessageSidFull(params: {
|
|
55
|
+
msgId?: string | null;
|
|
56
|
+
cliMsgId?: string | null;
|
|
57
|
+
}): string | undefined {
|
|
58
|
+
const msgId = toMessageSidPart(params.msgId);
|
|
59
|
+
const cliMsgId = toMessageSidPart(params.cliMsgId);
|
|
60
|
+
if (!msgId && !cliMsgId) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (msgId && cliMsgId) {
|
|
64
|
+
return `${msgId}:${cliMsgId}`;
|
|
65
|
+
}
|
|
66
|
+
return msgId || cliMsgId || undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveZalouserMessageSid(params: {
|
|
70
|
+
msgId?: string | null;
|
|
71
|
+
cliMsgId?: string | null;
|
|
72
|
+
fallback?: string | null;
|
|
73
|
+
}): string | undefined {
|
|
74
|
+
const msgId = toMessageSidPart(params.msgId);
|
|
75
|
+
const cliMsgId = toMessageSidPart(params.cliMsgId);
|
|
76
|
+
if (msgId || cliMsgId) {
|
|
77
|
+
return msgId || cliMsgId;
|
|
78
|
+
}
|
|
79
|
+
return toMessageSidPart(params.fallback) || undefined;
|
|
80
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { __testing } from "./monitor.js";
|
|
4
|
+
import { setZalouserRuntime } from "./runtime.js";
|
|
5
|
+
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const sendMessageZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
8
|
+
const sendTypingZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
9
|
+
const sendDeliveredZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
10
|
+
const sendSeenZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
11
|
+
|
|
12
|
+
vi.mock("./send.js", () => ({
|
|
13
|
+
sendMessageZalouser: sendMessageZalouserMock,
|
|
14
|
+
sendTypingZalouser: sendTypingZalouserMock,
|
|
15
|
+
sendDeliveredZalouser: sendDeliveredZalouserMock,
|
|
16
|
+
sendSeenZalouser: sendSeenZalouserMock,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("zalouser monitor pairing account scoping", () => {
|
|
20
|
+
it("scopes DM pairing-store reads and pairing requests to accountId", async () => {
|
|
21
|
+
const readAllowFromStore = vi.fn(
|
|
22
|
+
async (
|
|
23
|
+
channelOrParams:
|
|
24
|
+
| string
|
|
25
|
+
| {
|
|
26
|
+
channel?: string;
|
|
27
|
+
accountId?: string;
|
|
28
|
+
},
|
|
29
|
+
_env?: NodeJS.ProcessEnv,
|
|
30
|
+
accountId?: string,
|
|
31
|
+
) => {
|
|
32
|
+
const scopedAccountId =
|
|
33
|
+
typeof channelOrParams === "object" && channelOrParams !== null
|
|
34
|
+
? channelOrParams.accountId
|
|
35
|
+
: accountId;
|
|
36
|
+
return scopedAccountId === "beta" ? [] : ["attacker"];
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
const upsertPairingRequest = vi.fn(async () => ({ code: "PAIRME88", created: true }));
|
|
40
|
+
|
|
41
|
+
setZalouserRuntime({
|
|
42
|
+
logging: {
|
|
43
|
+
shouldLogVerbose: () => false,
|
|
44
|
+
},
|
|
45
|
+
channel: {
|
|
46
|
+
pairing: {
|
|
47
|
+
readAllowFromStore,
|
|
48
|
+
upsertPairingRequest,
|
|
49
|
+
buildPairingReply: vi.fn(() => "pairing reply"),
|
|
50
|
+
},
|
|
51
|
+
commands: {
|
|
52
|
+
shouldComputeCommandAuthorized: vi.fn(() => false),
|
|
53
|
+
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
|
|
54
|
+
isControlCommandMessage: vi.fn(() => false),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
} as unknown as PluginRuntime);
|
|
58
|
+
|
|
59
|
+
const account: ResolvedZalouserAccount = {
|
|
60
|
+
accountId: "beta",
|
|
61
|
+
enabled: true,
|
|
62
|
+
profile: "beta",
|
|
63
|
+
authenticated: true,
|
|
64
|
+
config: {
|
|
65
|
+
dmPolicy: "pairing",
|
|
66
|
+
allowFrom: [],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const config: OpenClawConfig = {
|
|
71
|
+
channels: {
|
|
72
|
+
zalouser: {
|
|
73
|
+
accounts: {
|
|
74
|
+
alpha: { dmPolicy: "pairing", allowFrom: [] },
|
|
75
|
+
beta: { dmPolicy: "pairing", allowFrom: [] },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const message: ZaloInboundMessage = {
|
|
82
|
+
threadId: "chat-1",
|
|
83
|
+
isGroup: false,
|
|
84
|
+
senderId: "attacker",
|
|
85
|
+
senderName: "Attacker",
|
|
86
|
+
groupName: undefined,
|
|
87
|
+
timestampMs: Date.now(),
|
|
88
|
+
msgId: "msg-1",
|
|
89
|
+
content: "hello",
|
|
90
|
+
raw: { source: "test" },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const runtime: RuntimeEnv = {
|
|
94
|
+
log: vi.fn(),
|
|
95
|
+
error: vi.fn(),
|
|
96
|
+
exit: ((code: number): never => {
|
|
97
|
+
throw new Error(`exit ${code}`);
|
|
98
|
+
}) as RuntimeEnv["exit"],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await __testing.processMessage({
|
|
102
|
+
message,
|
|
103
|
+
account,
|
|
104
|
+
config,
|
|
105
|
+
runtime,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(readAllowFromStore).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
channel: "zalouser",
|
|
111
|
+
accountId: "beta",
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
expect(upsertPairingRequest).toHaveBeenCalledWith(
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
channel: "zalouser",
|
|
117
|
+
id: "attacker",
|
|
118
|
+
accountId: "beta",
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(sendMessageZalouserMock).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { __testing } from "./monitor.js";
|
|
4
|
+
import { setZalouserRuntime } from "./runtime.js";
|
|
5
|
+
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const sendMessageZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
8
|
+
const sendTypingZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
9
|
+
const sendDeliveredZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
10
|
+
const sendSeenZalouserMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
11
|
+
|
|
12
|
+
vi.mock("./send.js", () => ({
|
|
13
|
+
sendMessageZalouser: sendMessageZalouserMock,
|
|
14
|
+
sendTypingZalouser: sendTypingZalouserMock,
|
|
15
|
+
sendDeliveredZalouser: sendDeliveredZalouserMock,
|
|
16
|
+
sendSeenZalouser: sendSeenZalouserMock,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
function createAccount(): ResolvedZalouserAccount {
|
|
20
|
+
return {
|
|
21
|
+
accountId: "default",
|
|
22
|
+
enabled: true,
|
|
23
|
+
profile: "default",
|
|
24
|
+
authenticated: true,
|
|
25
|
+
config: {
|
|
26
|
+
groupPolicy: "open",
|
|
27
|
+
groups: {
|
|
28
|
+
"*": { requireMention: true },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createConfig(): OpenClawConfig {
|
|
35
|
+
return {
|
|
36
|
+
channels: {
|
|
37
|
+
zalouser: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
groups: {
|
|
40
|
+
"*": { requireMention: true },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createRuntimeEnv(): RuntimeEnv {
|
|
48
|
+
return {
|
|
49
|
+
log: vi.fn(),
|
|
50
|
+
error: vi.fn(),
|
|
51
|
+
exit: ((code: number): never => {
|
|
52
|
+
throw new Error(`exit ${code}`);
|
|
53
|
+
}) as RuntimeEnv["exit"],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function installRuntime(params: { commandAuthorized: boolean }) {
|
|
58
|
+
const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions, ctx }) => {
|
|
59
|
+
await dispatcherOptions.typingCallbacks?.onReplyStart?.();
|
|
60
|
+
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 }, ctx };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
setZalouserRuntime({
|
|
64
|
+
logging: {
|
|
65
|
+
shouldLogVerbose: () => false,
|
|
66
|
+
},
|
|
67
|
+
channel: {
|
|
68
|
+
pairing: {
|
|
69
|
+
readAllowFromStore: vi.fn(async () => []),
|
|
70
|
+
upsertPairingRequest: vi.fn(async () => ({ code: "PAIR", created: true })),
|
|
71
|
+
buildPairingReply: vi.fn(() => "pair"),
|
|
72
|
+
},
|
|
73
|
+
commands: {
|
|
74
|
+
shouldComputeCommandAuthorized: vi.fn((body: string) => body.trim().startsWith("/")),
|
|
75
|
+
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => params.commandAuthorized),
|
|
76
|
+
isControlCommandMessage: vi.fn((body: string) => body.trim().startsWith("/")),
|
|
77
|
+
shouldHandleTextCommands: vi.fn(() => true),
|
|
78
|
+
},
|
|
79
|
+
mentions: {
|
|
80
|
+
buildMentionRegexes: vi.fn(() => []),
|
|
81
|
+
matchesMentionWithExplicit: vi.fn(
|
|
82
|
+
(input) => input.explicit?.isExplicitlyMentioned === true,
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
groups: {
|
|
86
|
+
resolveRequireMention: vi.fn((input) => {
|
|
87
|
+
const cfg = input.cfg as OpenClawConfig;
|
|
88
|
+
const groupCfg = cfg.channels?.zalouser?.groups ?? {};
|
|
89
|
+
const groupEntry = input.groupId ? groupCfg[input.groupId] : undefined;
|
|
90
|
+
const defaultEntry = groupCfg["*"];
|
|
91
|
+
if (typeof groupEntry?.requireMention === "boolean") {
|
|
92
|
+
return groupEntry.requireMention;
|
|
93
|
+
}
|
|
94
|
+
if (typeof defaultEntry?.requireMention === "boolean") {
|
|
95
|
+
return defaultEntry.requireMention;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
routing: {
|
|
101
|
+
resolveAgentRoute: vi.fn(() => ({
|
|
102
|
+
agentId: "main",
|
|
103
|
+
sessionKey: "agent:main:zalouser:group:1",
|
|
104
|
+
accountId: "default",
|
|
105
|
+
mainSessionKey: "agent:main:main",
|
|
106
|
+
})),
|
|
107
|
+
},
|
|
108
|
+
session: {
|
|
109
|
+
resolveStorePath: vi.fn(() => "/tmp"),
|
|
110
|
+
readSessionUpdatedAt: vi.fn(() => undefined),
|
|
111
|
+
recordInboundSession: vi.fn(async () => {}),
|
|
112
|
+
},
|
|
113
|
+
reply: {
|
|
114
|
+
resolveEnvelopeFormatOptions: vi.fn(() => undefined),
|
|
115
|
+
formatAgentEnvelope: vi.fn(({ body }) => body),
|
|
116
|
+
finalizeInboundContext: vi.fn((ctx) => ctx),
|
|
117
|
+
dispatchReplyWithBufferedBlockDispatcher,
|
|
118
|
+
},
|
|
119
|
+
text: {
|
|
120
|
+
resolveMarkdownTableMode: vi.fn(() => "code"),
|
|
121
|
+
convertMarkdownTables: vi.fn((text: string) => text),
|
|
122
|
+
resolveChunkMode: vi.fn(() => "line"),
|
|
123
|
+
chunkMarkdownTextWithMode: vi.fn((text: string) => [text]),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
} as unknown as PluginRuntime);
|
|
127
|
+
|
|
128
|
+
return { dispatchReplyWithBufferedBlockDispatcher };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createGroupMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
|
|
132
|
+
return {
|
|
133
|
+
threadId: "g-1",
|
|
134
|
+
isGroup: true,
|
|
135
|
+
senderId: "123",
|
|
136
|
+
senderName: "Alice",
|
|
137
|
+
groupName: "Team",
|
|
138
|
+
content: "hello",
|
|
139
|
+
timestampMs: Date.now(),
|
|
140
|
+
msgId: "m-1",
|
|
141
|
+
hasAnyMention: false,
|
|
142
|
+
wasExplicitlyMentioned: false,
|
|
143
|
+
canResolveExplicitMention: true,
|
|
144
|
+
implicitMention: false,
|
|
145
|
+
raw: { source: "test" },
|
|
146
|
+
...overrides,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
describe("zalouser monitor group mention gating", () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
sendMessageZalouserMock.mockClear();
|
|
153
|
+
sendTypingZalouserMock.mockClear();
|
|
154
|
+
sendDeliveredZalouserMock.mockClear();
|
|
155
|
+
sendSeenZalouserMock.mockClear();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("skips unmentioned group messages when requireMention=true", async () => {
|
|
159
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
160
|
+
commandAuthorized: false,
|
|
161
|
+
});
|
|
162
|
+
await __testing.processMessage({
|
|
163
|
+
message: createGroupMessage(),
|
|
164
|
+
account: createAccount(),
|
|
165
|
+
config: createConfig(),
|
|
166
|
+
runtime: createRuntimeEnv(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
170
|
+
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("dispatches explicitly-mentioned group messages and marks WasMentioned", async () => {
|
|
174
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
175
|
+
commandAuthorized: false,
|
|
176
|
+
});
|
|
177
|
+
await __testing.processMessage({
|
|
178
|
+
message: createGroupMessage({
|
|
179
|
+
hasAnyMention: true,
|
|
180
|
+
wasExplicitlyMentioned: true,
|
|
181
|
+
content: "ping @bot",
|
|
182
|
+
}),
|
|
183
|
+
account: createAccount(),
|
|
184
|
+
config: createConfig(),
|
|
185
|
+
runtime: createRuntimeEnv(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
189
|
+
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
190
|
+
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
191
|
+
expect(sendTypingZalouserMock).toHaveBeenCalledWith("g-1", {
|
|
192
|
+
profile: "default",
|
|
193
|
+
isGroup: true,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("allows authorized control commands to bypass mention gating", async () => {
|
|
198
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
199
|
+
commandAuthorized: true,
|
|
200
|
+
});
|
|
201
|
+
await __testing.processMessage({
|
|
202
|
+
message: createGroupMessage({
|
|
203
|
+
content: "/status",
|
|
204
|
+
hasAnyMention: false,
|
|
205
|
+
wasExplicitlyMentioned: false,
|
|
206
|
+
}),
|
|
207
|
+
account: createAccount(),
|
|
208
|
+
config: createConfig(),
|
|
209
|
+
runtime: createRuntimeEnv(),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
213
|
+
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
214
|
+
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
});
|