@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,117 @@
|
|
|
1
|
+
import type { ReplyPayload } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { zalouserPlugin } from "./channel.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./send.js", () => ({
|
|
6
|
+
sendMessageZalouser: vi.fn().mockResolvedValue({ ok: true, messageId: "zlu-1" }),
|
|
7
|
+
sendReactionZalouser: vi.fn().mockResolvedValue({ ok: true }),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("./accounts.js", async (importOriginal) => {
|
|
11
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
resolveZalouserAccountSync: () => ({
|
|
15
|
+
accountId: "default",
|
|
16
|
+
profile: "default",
|
|
17
|
+
name: "test",
|
|
18
|
+
enabled: true,
|
|
19
|
+
config: {},
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function baseCtx(payload: ReplyPayload) {
|
|
25
|
+
return {
|
|
26
|
+
cfg: {},
|
|
27
|
+
to: "987654321",
|
|
28
|
+
text: "",
|
|
29
|
+
payload,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("zalouserPlugin outbound sendPayload", () => {
|
|
34
|
+
let mockedSend: ReturnType<typeof vi.mocked<(typeof import("./send.js"))["sendMessageZalouser"]>>;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
const mod = await import("./send.js");
|
|
38
|
+
mockedSend = vi.mocked(mod.sendMessageZalouser);
|
|
39
|
+
mockedSend.mockClear();
|
|
40
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-1" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("text-only delegates to sendText", async () => {
|
|
44
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-t1" });
|
|
45
|
+
|
|
46
|
+
const result = await zalouserPlugin.outbound!.sendPayload!(baseCtx({ text: "hello" }));
|
|
47
|
+
|
|
48
|
+
expect(mockedSend).toHaveBeenCalledWith("987654321", "hello", expect.any(Object));
|
|
49
|
+
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-t1" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("single media delegates to sendMedia", async () => {
|
|
53
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-m1" });
|
|
54
|
+
|
|
55
|
+
const result = await zalouserPlugin.outbound!.sendPayload!(
|
|
56
|
+
baseCtx({ text: "cap", mediaUrl: "https://example.com/a.jpg" }),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
60
|
+
"987654321",
|
|
61
|
+
"cap",
|
|
62
|
+
expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }),
|
|
63
|
+
);
|
|
64
|
+
expect(result).toMatchObject({ channel: "zalouser" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("multi-media iterates URLs with caption on first", async () => {
|
|
68
|
+
mockedSend
|
|
69
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zlu-1" })
|
|
70
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zlu-2" });
|
|
71
|
+
|
|
72
|
+
const result = await zalouserPlugin.outbound!.sendPayload!(
|
|
73
|
+
baseCtx({
|
|
74
|
+
text: "caption",
|
|
75
|
+
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(mockedSend).toHaveBeenCalledTimes(2);
|
|
80
|
+
expect(mockedSend).toHaveBeenNthCalledWith(
|
|
81
|
+
1,
|
|
82
|
+
"987654321",
|
|
83
|
+
"caption",
|
|
84
|
+
expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }),
|
|
85
|
+
);
|
|
86
|
+
expect(mockedSend).toHaveBeenNthCalledWith(
|
|
87
|
+
2,
|
|
88
|
+
"987654321",
|
|
89
|
+
"",
|
|
90
|
+
expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }),
|
|
91
|
+
);
|
|
92
|
+
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-2" });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("empty payload returns no-op", async () => {
|
|
96
|
+
const result = await zalouserPlugin.outbound!.sendPayload!(baseCtx({}));
|
|
97
|
+
|
|
98
|
+
expect(mockedSend).not.toHaveBeenCalled();
|
|
99
|
+
expect(result).toEqual({ channel: "zalouser", messageId: "" });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("chunking splits long text", async () => {
|
|
103
|
+
mockedSend
|
|
104
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zlu-c1" })
|
|
105
|
+
.mockResolvedValueOnce({ ok: true, messageId: "zlu-c2" });
|
|
106
|
+
|
|
107
|
+
const longText = "a".repeat(3000);
|
|
108
|
+
const result = await zalouserPlugin.outbound!.sendPayload!(baseCtx({ text: longText }));
|
|
109
|
+
|
|
110
|
+
// textChunkLimit is 2000 with chunkTextForOutbound, so it should split
|
|
111
|
+
expect(mockedSend.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
112
|
+
for (const call of mockedSend.mock.calls) {
|
|
113
|
+
expect((call[1] as string).length).toBeLessThanOrEqual(2000);
|
|
114
|
+
}
|
|
115
|
+
expect(result).toMatchObject({ channel: "zalouser" });
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/channel.test.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { zalouserPlugin } from "./channel.js";
|
|
3
|
+
import { sendReactionZalouser } from "./send.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./send.js", async (importOriginal) => {
|
|
6
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
sendReactionZalouser: vi.fn(async () => ({ ok: true })),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const mockSendReaction = vi.mocked(sendReactionZalouser);
|
|
3
14
|
|
|
4
15
|
describe("zalouser outbound chunker", () => {
|
|
5
16
|
it("chunks without empty strings and respects limit", () => {
|
|
@@ -16,3 +27,114 @@ describe("zalouser outbound chunker", () => {
|
|
|
16
27
|
expect(chunks.every((c) => c.length <= limit)).toBe(true);
|
|
17
28
|
});
|
|
18
29
|
});
|
|
30
|
+
|
|
31
|
+
describe("zalouser channel policies", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockSendReaction.mockClear();
|
|
34
|
+
mockSendReaction.mockResolvedValue({ ok: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("resolves requireMention from group config", () => {
|
|
38
|
+
const resolveRequireMention = zalouserPlugin.groups?.resolveRequireMention;
|
|
39
|
+
expect(resolveRequireMention).toBeTypeOf("function");
|
|
40
|
+
if (!resolveRequireMention) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const requireMention = resolveRequireMention({
|
|
44
|
+
cfg: {
|
|
45
|
+
channels: {
|
|
46
|
+
zalouser: {
|
|
47
|
+
groups: {
|
|
48
|
+
"123": { requireMention: false },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
accountId: "default",
|
|
54
|
+
groupId: "123",
|
|
55
|
+
groupChannel: "123",
|
|
56
|
+
});
|
|
57
|
+
expect(requireMention).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("resolves group tool policy by explicit group id", () => {
|
|
61
|
+
const resolveToolPolicy = zalouserPlugin.groups?.resolveToolPolicy;
|
|
62
|
+
expect(resolveToolPolicy).toBeTypeOf("function");
|
|
63
|
+
if (!resolveToolPolicy) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const policy = resolveToolPolicy({
|
|
67
|
+
cfg: {
|
|
68
|
+
channels: {
|
|
69
|
+
zalouser: {
|
|
70
|
+
groups: {
|
|
71
|
+
"123": { tools: { allow: ["search"] } },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
accountId: "default",
|
|
77
|
+
groupId: "123",
|
|
78
|
+
groupChannel: "123",
|
|
79
|
+
});
|
|
80
|
+
expect(policy).toEqual({ allow: ["search"] });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("falls back to wildcard group policy", () => {
|
|
84
|
+
const resolveToolPolicy = zalouserPlugin.groups?.resolveToolPolicy;
|
|
85
|
+
expect(resolveToolPolicy).toBeTypeOf("function");
|
|
86
|
+
if (!resolveToolPolicy) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const policy = resolveToolPolicy({
|
|
90
|
+
cfg: {
|
|
91
|
+
channels: {
|
|
92
|
+
zalouser: {
|
|
93
|
+
groups: {
|
|
94
|
+
"*": { tools: { deny: ["system.run"] } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
accountId: "default",
|
|
100
|
+
groupId: "missing",
|
|
101
|
+
groupChannel: "missing",
|
|
102
|
+
});
|
|
103
|
+
expect(policy).toEqual({ deny: ["system.run"] });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("handles react action", async () => {
|
|
107
|
+
const actions = zalouserPlugin.actions;
|
|
108
|
+
expect(actions?.listActions?.({ cfg: { channels: { zalouser: { enabled: true } } } })).toEqual([
|
|
109
|
+
"react",
|
|
110
|
+
]);
|
|
111
|
+
const result = await actions?.handleAction?.({
|
|
112
|
+
channel: "zalouser",
|
|
113
|
+
action: "react",
|
|
114
|
+
params: {
|
|
115
|
+
threadId: "123456",
|
|
116
|
+
messageId: "111",
|
|
117
|
+
cliMsgId: "222",
|
|
118
|
+
emoji: "👍",
|
|
119
|
+
},
|
|
120
|
+
cfg: {
|
|
121
|
+
channels: {
|
|
122
|
+
zalouser: {
|
|
123
|
+
enabled: true,
|
|
124
|
+
profile: "default",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
expect(mockSendReaction).toHaveBeenCalledWith({
|
|
130
|
+
profile: "default",
|
|
131
|
+
threadId: "123456",
|
|
132
|
+
isGroup: false,
|
|
133
|
+
msgId: "111",
|
|
134
|
+
cliMsgId: "222",
|
|
135
|
+
emoji: "👍",
|
|
136
|
+
remove: false,
|
|
137
|
+
});
|
|
138
|
+
expect(result).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
});
|