@openclaw/zalouser 2026.3.11 → 2026.3.12
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 +7 -0
- package/package.json +1 -1
- package/src/channel.sendpayload.test.ts +36 -4
- package/src/channel.test.ts +46 -11
- package/src/channel.ts +21 -6
- package/src/config-schema.ts +1 -0
- package/src/group-policy.test.ts +12 -0
- package/src/group-policy.ts +7 -4
- package/src/monitor.group-gating.test.ts +109 -1
- package/src/monitor.ts +24 -14
- package/src/send.test.ts +247 -9
- package/src/send.ts +187 -2
- package/src/text-styles.test.ts +203 -0
- package/src/text-styles.ts +537 -0
- package/src/types.ts +7 -0
- package/src/zalo-js.ts +48 -2
- package/src/zca-client.ts +33 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
primeSendMock,
|
|
6
6
|
} from "../../../src/test-utils/send-payload-contract.js";
|
|
7
7
|
import { zalouserPlugin } from "./channel.js";
|
|
8
|
+
import { setZalouserRuntime } from "./runtime.js";
|
|
8
9
|
|
|
9
10
|
vi.mock("./send.js", () => ({
|
|
10
11
|
sendMessageZalouser: vi.fn().mockResolvedValue({ ok: true, messageId: "zlu-1" }),
|
|
@@ -38,6 +39,14 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
38
39
|
let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalouser"]>>;
|
|
39
40
|
|
|
40
41
|
beforeEach(async () => {
|
|
42
|
+
setZalouserRuntime({
|
|
43
|
+
channel: {
|
|
44
|
+
text: {
|
|
45
|
+
resolveChunkMode: vi.fn(() => "length"),
|
|
46
|
+
resolveTextChunkLimit: vi.fn(() => 1200),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} as never);
|
|
41
50
|
const mod = await import("./send.js");
|
|
42
51
|
mockedSend = vi.mocked(mod.sendMessageZalouser);
|
|
43
52
|
mockedSend.mockClear();
|
|
@@ -55,7 +64,7 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
55
64
|
expect(mockedSend).toHaveBeenCalledWith(
|
|
56
65
|
"1471383327500481391",
|
|
57
66
|
"hello group",
|
|
58
|
-
expect.objectContaining({ isGroup: true }),
|
|
67
|
+
expect.objectContaining({ isGroup: true, textMode: "markdown" }),
|
|
59
68
|
);
|
|
60
69
|
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g1" });
|
|
61
70
|
});
|
|
@@ -71,7 +80,7 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
71
80
|
expect(mockedSend).toHaveBeenCalledWith(
|
|
72
81
|
"987654321",
|
|
73
82
|
"hello",
|
|
74
|
-
expect.objectContaining({ isGroup: false }),
|
|
83
|
+
expect.objectContaining({ isGroup: false, textMode: "markdown" }),
|
|
75
84
|
);
|
|
76
85
|
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-d1" });
|
|
77
86
|
});
|
|
@@ -87,14 +96,37 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
87
96
|
expect(mockedSend).toHaveBeenCalledWith(
|
|
88
97
|
"g-1471383327500481391",
|
|
89
98
|
"hello native group",
|
|
90
|
-
expect.objectContaining({ isGroup: true }),
|
|
99
|
+
expect.objectContaining({ isGroup: true, textMode: "markdown" }),
|
|
91
100
|
);
|
|
92
101
|
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g-native" });
|
|
93
102
|
});
|
|
94
103
|
|
|
104
|
+
it("passes long markdown through once so formatting happens before chunking", async () => {
|
|
105
|
+
const text = `**${"a".repeat(2501)}**`;
|
|
106
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-code" });
|
|
107
|
+
|
|
108
|
+
const result = await zalouserPlugin.outbound!.sendPayload!({
|
|
109
|
+
...baseCtx({ text }),
|
|
110
|
+
to: "987654321",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(mockedSend).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
115
|
+
"987654321",
|
|
116
|
+
text,
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
isGroup: false,
|
|
119
|
+
textMode: "markdown",
|
|
120
|
+
textChunkMode: "length",
|
|
121
|
+
textChunkLimit: 1200,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-code" });
|
|
125
|
+
});
|
|
126
|
+
|
|
95
127
|
installSendPayloadContractSuite({
|
|
96
128
|
channel: "zalouser",
|
|
97
|
-
chunking: { mode: "
|
|
129
|
+
chunking: { mode: "passthrough", longTextLength: 3000 },
|
|
98
130
|
createHarness: ({ payload, sendResults }) => {
|
|
99
131
|
primeSendMock(mockedSend, { ok: true, messageId: "zlu-1" }, sendResults);
|
|
100
132
|
return {
|
package/src/channel.test.ts
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { zalouserPlugin } from "./channel.js";
|
|
3
|
-
import {
|
|
3
|
+
import { setZalouserRuntime } from "./runtime.js";
|
|
4
|
+
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
|
|
4
5
|
|
|
5
6
|
vi.mock("./send.js", async (importOriginal) => {
|
|
6
7
|
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
7
8
|
return {
|
|
8
9
|
...actual,
|
|
10
|
+
sendMessageZalouser: vi.fn(async () => ({ ok: true, messageId: "mid-1" })),
|
|
9
11
|
sendReactionZalouser: vi.fn(async () => ({ ok: true })),
|
|
10
12
|
};
|
|
11
13
|
});
|
|
12
14
|
|
|
15
|
+
const mockSendMessage = vi.mocked(sendMessageZalouser);
|
|
13
16
|
const mockSendReaction = vi.mocked(sendReactionZalouser);
|
|
14
17
|
|
|
15
|
-
describe("zalouser outbound
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
describe("zalouser outbound", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockSendMessage.mockClear();
|
|
21
|
+
setZalouserRuntime({
|
|
22
|
+
channel: {
|
|
23
|
+
text: {
|
|
24
|
+
resolveChunkMode: vi.fn(() => "newline"),
|
|
25
|
+
resolveTextChunkLimit: vi.fn(() => 10),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
} as never);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("passes markdown chunk settings through sendText", async () => {
|
|
32
|
+
const sendText = zalouserPlugin.outbound?.sendText;
|
|
33
|
+
expect(sendText).toBeTypeOf("function");
|
|
34
|
+
if (!sendText) {
|
|
20
35
|
return;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const result = await sendText({
|
|
39
|
+
cfg: { channels: { zalouser: { enabled: true } } } as never,
|
|
40
|
+
to: "group:123456",
|
|
41
|
+
text: "hello world\nthis is a test",
|
|
42
|
+
accountId: "default",
|
|
43
|
+
} as never);
|
|
44
|
+
|
|
45
|
+
expect(mockSendMessage).toHaveBeenCalledWith(
|
|
46
|
+
"123456",
|
|
47
|
+
"hello world\nthis is a test",
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
profile: "default",
|
|
50
|
+
isGroup: true,
|
|
51
|
+
textMode: "markdown",
|
|
52
|
+
textChunkMode: "newline",
|
|
53
|
+
textChunkLimit: 10,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
expect(result).toEqual(
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
channel: "zalouser",
|
|
59
|
+
messageId: "mid-1",
|
|
60
|
+
ok: true,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
28
63
|
});
|
|
29
64
|
});
|
|
30
65
|
|
package/src/channel.ts
CHANGED
|
@@ -20,9 +20,9 @@ import {
|
|
|
20
20
|
buildBaseAccountStatusSnapshot,
|
|
21
21
|
buildChannelConfigSchema,
|
|
22
22
|
DEFAULT_ACCOUNT_ID,
|
|
23
|
-
chunkTextForOutbound,
|
|
24
23
|
deleteAccountFromConfigSection,
|
|
25
24
|
formatAllowFromLowercase,
|
|
25
|
+
isDangerousNameMatchingEnabled,
|
|
26
26
|
isNumericTargetId,
|
|
27
27
|
migrateBaseNameToDefaultAccount,
|
|
28
28
|
normalizeAccountId,
|
|
@@ -43,6 +43,7 @@ import { resolveZalouserReactionMessageIds } from "./message-sid.js";
|
|
|
43
43
|
import { zalouserOnboardingAdapter } from "./onboarding.js";
|
|
44
44
|
import { probeZalouser } from "./probe.js";
|
|
45
45
|
import { writeQrDataUrlToTempFile } from "./qr-temp-file.js";
|
|
46
|
+
import { getZalouserRuntime } from "./runtime.js";
|
|
46
47
|
import { sendMessageZalouser, sendReactionZalouser } from "./send.js";
|
|
47
48
|
import { collectZalouserStatusIssues } from "./status-issues.js";
|
|
48
49
|
import {
|
|
@@ -166,6 +167,16 @@ function resolveZalouserQrProfile(accountId?: string | null): string {
|
|
|
166
167
|
return normalized;
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
function resolveZalouserOutboundChunkMode(cfg: OpenClawConfig, accountId?: string) {
|
|
171
|
+
return getZalouserRuntime().channel.text.resolveChunkMode(cfg, "zalouser", accountId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveZalouserOutboundTextChunkLimit(cfg: OpenClawConfig, accountId?: string) {
|
|
175
|
+
return getZalouserRuntime().channel.text.resolveTextChunkLimit(cfg, "zalouser", accountId, {
|
|
176
|
+
fallbackLimit: zalouserDock.outbound?.textChunkLimit ?? 2000,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
169
180
|
function mapUser(params: {
|
|
170
181
|
id: string;
|
|
171
182
|
name?: string | null;
|
|
@@ -206,6 +217,7 @@ function resolveZalouserGroupPolicyEntry(params: ChannelGroupContext) {
|
|
|
206
217
|
groupId: params.groupId,
|
|
207
218
|
groupChannel: params.groupChannel,
|
|
208
219
|
includeWildcard: true,
|
|
220
|
+
allowNameMatching: isDangerousNameMatchingEnabled(account.config),
|
|
209
221
|
}),
|
|
210
222
|
);
|
|
211
223
|
}
|
|
@@ -595,14 +607,11 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
595
607
|
},
|
|
596
608
|
outbound: {
|
|
597
609
|
deliveryMode: "direct",
|
|
598
|
-
chunker:
|
|
599
|
-
chunkerMode: "
|
|
600
|
-
textChunkLimit: 2000,
|
|
610
|
+
chunker: (text, limit) => getZalouserRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
611
|
+
chunkerMode: "markdown",
|
|
601
612
|
sendPayload: async (ctx) =>
|
|
602
613
|
await sendPayloadWithChunkedTextAndMedia({
|
|
603
614
|
ctx,
|
|
604
|
-
textChunkLimit: zalouserPlugin.outbound!.textChunkLimit,
|
|
605
|
-
chunker: zalouserPlugin.outbound!.chunker,
|
|
606
615
|
sendText: (nextCtx) => zalouserPlugin.outbound!.sendText!(nextCtx),
|
|
607
616
|
sendMedia: (nextCtx) => zalouserPlugin.outbound!.sendMedia!(nextCtx),
|
|
608
617
|
emptyResult: { channel: "zalouser", messageId: "" },
|
|
@@ -613,6 +622,9 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
613
622
|
const result = await sendMessageZalouser(target.threadId, text, {
|
|
614
623
|
profile: account.profile,
|
|
615
624
|
isGroup: target.isGroup,
|
|
625
|
+
textMode: "markdown",
|
|
626
|
+
textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
|
|
627
|
+
textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId),
|
|
616
628
|
});
|
|
617
629
|
return buildChannelSendResult("zalouser", result);
|
|
618
630
|
},
|
|
@@ -624,6 +636,9 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|
|
624
636
|
isGroup: target.isGroup,
|
|
625
637
|
mediaUrl,
|
|
626
638
|
mediaLocalRoots,
|
|
639
|
+
textMode: "markdown",
|
|
640
|
+
textChunkMode: resolveZalouserOutboundChunkMode(cfg, account.accountId),
|
|
641
|
+
textChunkLimit: resolveZalouserOutboundTextChunkLimit(cfg, account.accountId),
|
|
627
642
|
});
|
|
628
643
|
return buildChannelSendResult("zalouser", result);
|
|
629
644
|
},
|
package/src/config-schema.ts
CHANGED
|
@@ -19,6 +19,7 @@ const zalouserAccountSchema = z.object({
|
|
|
19
19
|
enabled: z.boolean().optional(),
|
|
20
20
|
markdown: MarkdownConfigSchema,
|
|
21
21
|
profile: z.string().optional(),
|
|
22
|
+
dangerouslyAllowNameMatching: z.boolean().optional(),
|
|
22
23
|
dmPolicy: DmPolicySchema.optional(),
|
|
23
24
|
allowFrom: AllowFromListSchema,
|
|
24
25
|
historyLimit: z.number().int().min(0).optional(),
|
package/src/group-policy.test.ts
CHANGED
|
@@ -23,6 +23,18 @@ describe("zalouser group policy helpers", () => {
|
|
|
23
23
|
).toEqual(["123", "group:123", "chan-1", "Team Alpha", "team-alpha", "*"]);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
it("builds id-only candidates when name matching is disabled", () => {
|
|
27
|
+
expect(
|
|
28
|
+
buildZalouserGroupCandidates({
|
|
29
|
+
groupId: "123",
|
|
30
|
+
groupChannel: "chan-1",
|
|
31
|
+
groupName: "Team Alpha",
|
|
32
|
+
includeGroupIdAlias: true,
|
|
33
|
+
allowNameMatching: false,
|
|
34
|
+
}),
|
|
35
|
+
).toEqual(["123", "group:123", "*"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
26
38
|
it("finds the first matching group entry", () => {
|
|
27
39
|
const groups = {
|
|
28
40
|
"group:123": { allow: true },
|
package/src/group-policy.ts
CHANGED
|
@@ -23,6 +23,7 @@ export function buildZalouserGroupCandidates(params: {
|
|
|
23
23
|
groupName?: string | null;
|
|
24
24
|
includeGroupIdAlias?: boolean;
|
|
25
25
|
includeWildcard?: boolean;
|
|
26
|
+
allowNameMatching?: boolean;
|
|
26
27
|
}): string[] {
|
|
27
28
|
const seen = new Set<string>();
|
|
28
29
|
const out: string[] = [];
|
|
@@ -43,10 +44,12 @@ export function buildZalouserGroupCandidates(params: {
|
|
|
43
44
|
if (params.includeGroupIdAlias === true && groupId) {
|
|
44
45
|
push(`group:${groupId}`);
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (params.allowNameMatching !== false) {
|
|
48
|
+
push(groupChannel);
|
|
49
|
+
push(groupName);
|
|
50
|
+
if (groupName) {
|
|
51
|
+
push(normalizeZalouserGroupSlug(groupName));
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
if (params.includeWildcard !== false) {
|
|
52
55
|
push("*");
|
|
@@ -51,6 +51,7 @@ function createRuntimeEnv(): RuntimeEnv {
|
|
|
51
51
|
|
|
52
52
|
function installRuntime(params: {
|
|
53
53
|
commandAuthorized?: boolean;
|
|
54
|
+
replyPayload?: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
54
55
|
resolveCommandAuthorizedFromAuthorizers?: (params: {
|
|
55
56
|
useAccessGroups: boolean;
|
|
56
57
|
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
@@ -58,6 +59,9 @@ function installRuntime(params: {
|
|
|
58
59
|
}) {
|
|
59
60
|
const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions, ctx }) => {
|
|
60
61
|
await dispatcherOptions.typingCallbacks?.onReplyStart?.();
|
|
62
|
+
if (params.replyPayload) {
|
|
63
|
+
await dispatcherOptions.deliver(params.replyPayload);
|
|
64
|
+
}
|
|
61
65
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 }, ctx };
|
|
62
66
|
});
|
|
63
67
|
const resolveCommandAuthorizedFromAuthorizers = vi.fn(
|
|
@@ -166,7 +170,8 @@ function installRuntime(params: {
|
|
|
166
170
|
text: {
|
|
167
171
|
resolveMarkdownTableMode: vi.fn(() => "code"),
|
|
168
172
|
convertMarkdownTables: vi.fn((text: string) => text),
|
|
169
|
-
resolveChunkMode: vi.fn(() => "
|
|
173
|
+
resolveChunkMode: vi.fn(() => "length"),
|
|
174
|
+
resolveTextChunkLimit: vi.fn(() => 1200),
|
|
170
175
|
chunkMarkdownTextWithMode: vi.fn((text: string) => [text]),
|
|
171
176
|
},
|
|
172
177
|
},
|
|
@@ -304,6 +309,42 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
304
309
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
305
310
|
});
|
|
306
311
|
|
|
312
|
+
it("passes long markdown replies through once so formatting happens before chunking", async () => {
|
|
313
|
+
const replyText = `**${"a".repeat(2501)}**`;
|
|
314
|
+
installRuntime({
|
|
315
|
+
commandAuthorized: false,
|
|
316
|
+
replyPayload: { text: replyText },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await __testing.processMessage({
|
|
320
|
+
message: createDmMessage({
|
|
321
|
+
content: "hello",
|
|
322
|
+
}),
|
|
323
|
+
account: {
|
|
324
|
+
...createAccount(),
|
|
325
|
+
config: {
|
|
326
|
+
...createAccount().config,
|
|
327
|
+
dmPolicy: "open",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
config: createConfig(),
|
|
331
|
+
runtime: createRuntimeEnv(),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(sendMessageZalouserMock).toHaveBeenCalledTimes(1);
|
|
335
|
+
expect(sendMessageZalouserMock).toHaveBeenCalledWith(
|
|
336
|
+
"u-1",
|
|
337
|
+
replyText,
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
isGroup: false,
|
|
340
|
+
profile: "default",
|
|
341
|
+
textMode: "markdown",
|
|
342
|
+
textChunkMode: "length",
|
|
343
|
+
textChunkLimit: 1200,
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
307
348
|
it("uses commandContent for mention-prefixed control commands", async () => {
|
|
308
349
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
309
350
|
commandAuthorized: true,
|
|
@@ -383,6 +424,73 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
383
424
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
384
425
|
});
|
|
385
426
|
|
|
427
|
+
it("does not accept a different group id by matching only the mutable group name by default", async () => {
|
|
428
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
429
|
+
commandAuthorized: false,
|
|
430
|
+
});
|
|
431
|
+
await __testing.processMessage({
|
|
432
|
+
message: createGroupMessage({
|
|
433
|
+
threadId: "g-attacker-001",
|
|
434
|
+
groupName: "Trusted Team",
|
|
435
|
+
senderId: "666",
|
|
436
|
+
hasAnyMention: true,
|
|
437
|
+
wasExplicitlyMentioned: true,
|
|
438
|
+
content: "ping @bot",
|
|
439
|
+
}),
|
|
440
|
+
account: {
|
|
441
|
+
...createAccount(),
|
|
442
|
+
config: {
|
|
443
|
+
...createAccount().config,
|
|
444
|
+
groupPolicy: "allowlist",
|
|
445
|
+
groupAllowFrom: ["*"],
|
|
446
|
+
groups: {
|
|
447
|
+
"group:g-trusted-001": { allow: true },
|
|
448
|
+
"Trusted Team": { allow: true },
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
config: createConfig(),
|
|
453
|
+
runtime: createRuntimeEnv(),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
|
|
460
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
461
|
+
commandAuthorized: false,
|
|
462
|
+
});
|
|
463
|
+
await __testing.processMessage({
|
|
464
|
+
message: createGroupMessage({
|
|
465
|
+
threadId: "g-attacker-001",
|
|
466
|
+
groupName: "Trusted Team",
|
|
467
|
+
senderId: "666",
|
|
468
|
+
hasAnyMention: true,
|
|
469
|
+
wasExplicitlyMentioned: true,
|
|
470
|
+
content: "ping @bot",
|
|
471
|
+
}),
|
|
472
|
+
account: {
|
|
473
|
+
...createAccount(),
|
|
474
|
+
config: {
|
|
475
|
+
...createAccount().config,
|
|
476
|
+
dangerouslyAllowNameMatching: true,
|
|
477
|
+
groupPolicy: "allowlist",
|
|
478
|
+
groupAllowFrom: ["*"],
|
|
479
|
+
groups: {
|
|
480
|
+
"group:g-trusted-001": { allow: true },
|
|
481
|
+
"Trusted Team": { allow: true },
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
config: createConfig(),
|
|
486
|
+
runtime: createRuntimeEnv(),
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
490
|
+
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
491
|
+
expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
|
|
492
|
+
});
|
|
493
|
+
|
|
386
494
|
it("allows group control commands when sender is in groupAllowFrom", async () => {
|
|
387
495
|
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
|
|
388
496
|
installRuntime({
|
package/src/monitor.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
createScopedPairingAccess,
|
|
20
20
|
createReplyPrefixOptions,
|
|
21
21
|
evaluateGroupRouteAccessForPolicy,
|
|
22
|
+
isDangerousNameMatchingEnabled,
|
|
22
23
|
issuePairingChallenge,
|
|
23
24
|
resolveOutboundMediaUrls,
|
|
24
25
|
mergeAllowlist,
|
|
@@ -212,6 +213,7 @@ function resolveGroupRequireMention(params: {
|
|
|
212
213
|
groupId: string;
|
|
213
214
|
groupName?: string | null;
|
|
214
215
|
groups: Record<string, { allow?: boolean; enabled?: boolean; requireMention?: boolean }>;
|
|
216
|
+
allowNameMatching?: boolean;
|
|
215
217
|
}): boolean {
|
|
216
218
|
const entry = findZalouserGroupEntry(
|
|
217
219
|
params.groups ?? {},
|
|
@@ -220,6 +222,7 @@ function resolveGroupRequireMention(params: {
|
|
|
220
222
|
groupName: params.groupName,
|
|
221
223
|
includeGroupIdAlias: true,
|
|
222
224
|
includeWildcard: true,
|
|
225
|
+
allowNameMatching: params.allowNameMatching,
|
|
223
226
|
}),
|
|
224
227
|
);
|
|
225
228
|
if (typeof entry?.requireMention === "boolean") {
|
|
@@ -316,6 +319,7 @@ async function processMessage(
|
|
|
316
319
|
});
|
|
317
320
|
|
|
318
321
|
const groups = account.config.groups ?? {};
|
|
322
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
319
323
|
if (isGroup) {
|
|
320
324
|
const groupEntry = findZalouserGroupEntry(
|
|
321
325
|
groups,
|
|
@@ -324,6 +328,7 @@ async function processMessage(
|
|
|
324
328
|
groupName,
|
|
325
329
|
includeGroupIdAlias: true,
|
|
326
330
|
includeWildcard: true,
|
|
331
|
+
allowNameMatching,
|
|
327
332
|
}),
|
|
328
333
|
);
|
|
329
334
|
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
|
@@ -466,6 +471,7 @@ async function processMessage(
|
|
|
466
471
|
groupId: chatId,
|
|
467
472
|
groupName,
|
|
468
473
|
groups,
|
|
474
|
+
allowNameMatching,
|
|
469
475
|
})
|
|
470
476
|
: false;
|
|
471
477
|
const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
|
|
@@ -703,6 +709,10 @@ async function deliverZalouserReply(params: {
|
|
|
703
709
|
params;
|
|
704
710
|
const tableMode = params.tableMode ?? "code";
|
|
705
711
|
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
|
712
|
+
const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
|
|
713
|
+
const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, {
|
|
714
|
+
fallbackLimit: ZALOUSER_TEXT_LIMIT,
|
|
715
|
+
});
|
|
706
716
|
|
|
707
717
|
const sentMedia = await sendMediaWithLeadingCaption({
|
|
708
718
|
mediaUrls: resolveOutboundMediaUrls(payload),
|
|
@@ -713,6 +723,9 @@ async function deliverZalouserReply(params: {
|
|
|
713
723
|
profile,
|
|
714
724
|
mediaUrl,
|
|
715
725
|
isGroup,
|
|
726
|
+
textMode: "markdown",
|
|
727
|
+
textChunkMode: chunkMode,
|
|
728
|
+
textChunkLimit,
|
|
716
729
|
});
|
|
717
730
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
718
731
|
},
|
|
@@ -725,20 +738,17 @@ async function deliverZalouserReply(params: {
|
|
|
725
738
|
}
|
|
726
739
|
|
|
727
740
|
if (text) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
} catch (err) {
|
|
740
|
-
runtime.error(`Zalouser message send failed: ${String(err)}`);
|
|
741
|
-
}
|
|
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)}`);
|
|
742
752
|
}
|
|
743
753
|
}
|
|
744
754
|
}
|