@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
package/src/tool.test.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { sendImageZalouser, sendLinkZalouser, sendMessageZalouser } from "./send.js";
|
|
3
|
+
import { executeZalouserTool } from "./tool.js";
|
|
4
|
+
import {
|
|
5
|
+
checkZaloAuthenticated,
|
|
6
|
+
getZaloUserInfo,
|
|
7
|
+
listZaloFriendsMatching,
|
|
8
|
+
listZaloGroupsMatching,
|
|
9
|
+
} from "./zalo-js.js";
|
|
10
|
+
|
|
11
|
+
vi.mock("./send.js", () => ({
|
|
12
|
+
sendMessageZalouser: vi.fn(),
|
|
13
|
+
sendImageZalouser: vi.fn(),
|
|
14
|
+
sendLinkZalouser: vi.fn(),
|
|
15
|
+
sendReactionZalouser: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("./zalo-js.js", () => ({
|
|
19
|
+
checkZaloAuthenticated: vi.fn(),
|
|
20
|
+
getZaloUserInfo: vi.fn(),
|
|
21
|
+
listZaloFriendsMatching: vi.fn(),
|
|
22
|
+
listZaloGroupsMatching: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const mockSendMessage = vi.mocked(sendMessageZalouser);
|
|
26
|
+
const mockSendImage = vi.mocked(sendImageZalouser);
|
|
27
|
+
const mockSendLink = vi.mocked(sendLinkZalouser);
|
|
28
|
+
const mockCheckAuth = vi.mocked(checkZaloAuthenticated);
|
|
29
|
+
const mockGetUserInfo = vi.mocked(getZaloUserInfo);
|
|
30
|
+
const mockListFriends = vi.mocked(listZaloFriendsMatching);
|
|
31
|
+
const mockListGroups = vi.mocked(listZaloGroupsMatching);
|
|
32
|
+
|
|
33
|
+
function extractDetails(result: Awaited<ReturnType<typeof executeZalouserTool>>): unknown {
|
|
34
|
+
const text = result.content[0]?.text ?? "{}";
|
|
35
|
+
return JSON.parse(text) as unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("executeZalouserTool", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockSendMessage.mockReset();
|
|
41
|
+
mockSendImage.mockReset();
|
|
42
|
+
mockSendLink.mockReset();
|
|
43
|
+
mockCheckAuth.mockReset();
|
|
44
|
+
mockGetUserInfo.mockReset();
|
|
45
|
+
mockListFriends.mockReset();
|
|
46
|
+
mockListGroups.mockReset();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns error when send action is missing required fields", async () => {
|
|
50
|
+
const result = await executeZalouserTool("tool-1", { action: "send" });
|
|
51
|
+
expect(extractDetails(result)).toEqual({
|
|
52
|
+
error: "threadId and message required for send action",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("sends text message for send action", async () => {
|
|
57
|
+
mockSendMessage.mockResolvedValueOnce({ ok: true, messageId: "m-1" });
|
|
58
|
+
const result = await executeZalouserTool("tool-1", {
|
|
59
|
+
action: "send",
|
|
60
|
+
threadId: "t-1",
|
|
61
|
+
message: "hello",
|
|
62
|
+
profile: "work",
|
|
63
|
+
isGroup: true,
|
|
64
|
+
});
|
|
65
|
+
expect(mockSendMessage).toHaveBeenCalledWith("t-1", "hello", {
|
|
66
|
+
profile: "work",
|
|
67
|
+
isGroup: true,
|
|
68
|
+
});
|
|
69
|
+
expect(extractDetails(result)).toEqual({ success: true, messageId: "m-1" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns tool error when send action fails", async () => {
|
|
73
|
+
mockSendMessage.mockResolvedValueOnce({ ok: false, error: "blocked" });
|
|
74
|
+
const result = await executeZalouserTool("tool-1", {
|
|
75
|
+
action: "send",
|
|
76
|
+
threadId: "t-1",
|
|
77
|
+
message: "hello",
|
|
78
|
+
});
|
|
79
|
+
expect(extractDetails(result)).toEqual({ error: "blocked" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("routes image and link actions to correct helpers", async () => {
|
|
83
|
+
mockSendImage.mockResolvedValueOnce({ ok: true, messageId: "img-1" });
|
|
84
|
+
const imageResult = await executeZalouserTool("tool-1", {
|
|
85
|
+
action: "image",
|
|
86
|
+
threadId: "g-1",
|
|
87
|
+
url: "https://example.com/image.jpg",
|
|
88
|
+
message: "caption",
|
|
89
|
+
isGroup: true,
|
|
90
|
+
});
|
|
91
|
+
expect(mockSendImage).toHaveBeenCalledWith("g-1", "https://example.com/image.jpg", {
|
|
92
|
+
profile: undefined,
|
|
93
|
+
caption: "caption",
|
|
94
|
+
isGroup: true,
|
|
95
|
+
});
|
|
96
|
+
expect(extractDetails(imageResult)).toEqual({ success: true, messageId: "img-1" });
|
|
97
|
+
|
|
98
|
+
mockSendLink.mockResolvedValueOnce({ ok: true, messageId: "lnk-1" });
|
|
99
|
+
const linkResult = await executeZalouserTool("tool-1", {
|
|
100
|
+
action: "link",
|
|
101
|
+
threadId: "t-2",
|
|
102
|
+
url: "https://openclaw.ai",
|
|
103
|
+
message: "read this",
|
|
104
|
+
});
|
|
105
|
+
expect(mockSendLink).toHaveBeenCalledWith("t-2", "https://openclaw.ai", {
|
|
106
|
+
profile: undefined,
|
|
107
|
+
caption: "read this",
|
|
108
|
+
isGroup: undefined,
|
|
109
|
+
});
|
|
110
|
+
expect(extractDetails(linkResult)).toEqual({ success: true, messageId: "lnk-1" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns friends/groups lists", async () => {
|
|
114
|
+
mockListFriends.mockResolvedValueOnce([{ userId: "1", displayName: "Alice" }]);
|
|
115
|
+
mockListGroups.mockResolvedValueOnce([{ groupId: "2", name: "Work" }]);
|
|
116
|
+
|
|
117
|
+
const friends = await executeZalouserTool("tool-1", {
|
|
118
|
+
action: "friends",
|
|
119
|
+
profile: "work",
|
|
120
|
+
query: "ali",
|
|
121
|
+
});
|
|
122
|
+
expect(mockListFriends).toHaveBeenCalledWith("work", "ali");
|
|
123
|
+
expect(extractDetails(friends)).toEqual([{ userId: "1", displayName: "Alice" }]);
|
|
124
|
+
|
|
125
|
+
const groups = await executeZalouserTool("tool-1", {
|
|
126
|
+
action: "groups",
|
|
127
|
+
profile: "work",
|
|
128
|
+
query: "wrk",
|
|
129
|
+
});
|
|
130
|
+
expect(mockListGroups).toHaveBeenCalledWith("work", "wrk");
|
|
131
|
+
expect(extractDetails(groups)).toEqual([{ groupId: "2", name: "Work" }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("reports me + status actions", async () => {
|
|
135
|
+
mockGetUserInfo.mockResolvedValueOnce({ userId: "7", displayName: "Me" });
|
|
136
|
+
mockCheckAuth.mockResolvedValueOnce(true);
|
|
137
|
+
|
|
138
|
+
const me = await executeZalouserTool("tool-1", { action: "me", profile: "work" });
|
|
139
|
+
expect(mockGetUserInfo).toHaveBeenCalledWith("work");
|
|
140
|
+
expect(extractDetails(me)).toEqual({ userId: "7", displayName: "Me" });
|
|
141
|
+
|
|
142
|
+
const status = await executeZalouserTool("tool-1", { action: "status", profile: "work" });
|
|
143
|
+
expect(mockCheckAuth).toHaveBeenCalledWith("work");
|
|
144
|
+
expect(extractDetails(status)).toEqual({
|
|
145
|
+
authenticated: true,
|
|
146
|
+
output: "authenticated",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
package/src/tool.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import {
|
|
2
|
+
import { sendImageZalouser, sendLinkZalouser, sendMessageZalouser } from "./send.js";
|
|
3
|
+
import {
|
|
4
|
+
checkZaloAuthenticated,
|
|
5
|
+
getZaloUserInfo,
|
|
6
|
+
listZaloFriendsMatching,
|
|
7
|
+
listZaloGroupsMatching,
|
|
8
|
+
} from "./zalo-js.js";
|
|
3
9
|
|
|
4
10
|
const ACTIONS = ["send", "image", "link", "friends", "groups", "me", "status"] as const;
|
|
5
11
|
|
|
@@ -19,7 +25,6 @@ function stringEnum<T extends readonly string[]>(
|
|
|
19
25
|
});
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
// Tool schema - avoiding Type.Union per tool schema guardrails
|
|
23
28
|
export const ZalouserToolSchema = Type.Object(
|
|
24
29
|
{
|
|
25
30
|
action: stringEnum(ACTIONS, { description: `Action to perform: ${ACTIONS.join(", ")}` }),
|
|
@@ -62,15 +67,14 @@ export async function executeZalouserTool(
|
|
|
62
67
|
if (!params.threadId || !params.message) {
|
|
63
68
|
throw new Error("threadId and message required for send action");
|
|
64
69
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
const result = await runZca(args, { profile: params.profile });
|
|
70
|
+
const result = await sendMessageZalouser(params.threadId, params.message, {
|
|
71
|
+
profile: params.profile,
|
|
72
|
+
isGroup: params.isGroup,
|
|
73
|
+
});
|
|
70
74
|
if (!result.ok) {
|
|
71
|
-
throw new Error(result.
|
|
75
|
+
throw new Error(result.error || "Failed to send message");
|
|
72
76
|
}
|
|
73
|
-
return json({ success: true,
|
|
77
|
+
return json({ success: true, messageId: result.messageId });
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
case "image": {
|
|
@@ -80,74 +84,52 @@ export async function executeZalouserTool(
|
|
|
80
84
|
if (!params.url) {
|
|
81
85
|
throw new Error("url required for image action");
|
|
82
86
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
args.push("-g");
|
|
89
|
-
}
|
|
90
|
-
const result = await runZca(args, { profile: params.profile });
|
|
87
|
+
const result = await sendImageZalouser(params.threadId, params.url, {
|
|
88
|
+
profile: params.profile,
|
|
89
|
+
caption: params.message,
|
|
90
|
+
isGroup: params.isGroup,
|
|
91
|
+
});
|
|
91
92
|
if (!result.ok) {
|
|
92
|
-
throw new Error(result.
|
|
93
|
+
throw new Error(result.error || "Failed to send image");
|
|
93
94
|
}
|
|
94
|
-
return json({ success: true,
|
|
95
|
+
return json({ success: true, messageId: result.messageId });
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
case "link": {
|
|
98
99
|
if (!params.threadId || !params.url) {
|
|
99
100
|
throw new Error("threadId and url required for link action");
|
|
100
101
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
const result = await sendLinkZalouser(params.threadId, params.url, {
|
|
103
|
+
profile: params.profile,
|
|
104
|
+
caption: params.message,
|
|
105
|
+
isGroup: params.isGroup,
|
|
106
|
+
});
|
|
106
107
|
if (!result.ok) {
|
|
107
|
-
throw new Error(result.
|
|
108
|
+
throw new Error(result.error || "Failed to send link");
|
|
108
109
|
}
|
|
109
|
-
return json({ success: true,
|
|
110
|
+
return json({ success: true, messageId: result.messageId });
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
case "friends": {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
if (!result.ok) {
|
|
116
|
-
throw new Error(result.stderr || "Failed to get friends");
|
|
117
|
-
}
|
|
118
|
-
const parsed = parseJsonOutput(result.stdout);
|
|
119
|
-
return json(parsed ?? { raw: result.stdout });
|
|
114
|
+
const rows = await listZaloFriendsMatching(params.profile, params.query);
|
|
115
|
+
return json(rows);
|
|
120
116
|
}
|
|
121
117
|
|
|
122
118
|
case "groups": {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
if (!result.ok) {
|
|
127
|
-
throw new Error(result.stderr || "Failed to get groups");
|
|
128
|
-
}
|
|
129
|
-
const parsed = parseJsonOutput(result.stdout);
|
|
130
|
-
return json(parsed ?? { raw: result.stdout });
|
|
119
|
+
const rows = await listZaloGroupsMatching(params.profile, params.query);
|
|
120
|
+
return json(rows);
|
|
131
121
|
}
|
|
132
122
|
|
|
133
123
|
case "me": {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
if (!result.ok) {
|
|
138
|
-
throw new Error(result.stderr || "Failed to get profile");
|
|
139
|
-
}
|
|
140
|
-
const parsed = parseJsonOutput(result.stdout);
|
|
141
|
-
return json(parsed ?? { raw: result.stdout });
|
|
124
|
+
const info = await getZaloUserInfo(params.profile);
|
|
125
|
+
return json(info ?? { error: "Not authenticated" });
|
|
142
126
|
}
|
|
143
127
|
|
|
144
128
|
case "status": {
|
|
145
|
-
const
|
|
146
|
-
profile: params.profile,
|
|
147
|
-
});
|
|
129
|
+
const authenticated = await checkZaloAuthenticated(params.profile);
|
|
148
130
|
return json({
|
|
149
|
-
authenticated
|
|
150
|
-
output:
|
|
131
|
+
authenticated,
|
|
132
|
+
output: authenticated ? "authenticated" : "not authenticated",
|
|
151
133
|
});
|
|
152
134
|
}
|
|
153
135
|
|
package/src/types.ts
CHANGED
|
@@ -1,48 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
timeout?: number;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type ZcaResult = {
|
|
9
|
-
ok: boolean;
|
|
10
|
-
stdout: string;
|
|
11
|
-
stderr: string;
|
|
12
|
-
exitCode: number;
|
|
1
|
+
export type ZcaFriend = {
|
|
2
|
+
userId: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
avatar?: string;
|
|
13
5
|
};
|
|
14
6
|
|
|
15
|
-
export type
|
|
7
|
+
export type ZaloGroup = {
|
|
8
|
+
groupId: string;
|
|
16
9
|
name: string;
|
|
17
|
-
|
|
18
|
-
isDefault?: boolean;
|
|
10
|
+
memberCount?: number;
|
|
19
11
|
};
|
|
20
12
|
|
|
21
|
-
export type
|
|
13
|
+
export type ZaloGroupMember = {
|
|
22
14
|
userId: string;
|
|
23
15
|
displayName: string;
|
|
24
16
|
avatar?: string;
|
|
25
17
|
};
|
|
26
18
|
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
export type ZaloEventMessage = {
|
|
20
|
+
msgId: string;
|
|
21
|
+
cliMsgId: string;
|
|
22
|
+
uidFrom: string;
|
|
23
|
+
idTo: string;
|
|
24
|
+
msgType: string;
|
|
25
|
+
st: number;
|
|
26
|
+
at: number;
|
|
27
|
+
cmd: number;
|
|
28
|
+
ts: string | number;
|
|
31
29
|
};
|
|
32
30
|
|
|
33
|
-
export type
|
|
31
|
+
export type ZaloInboundMessage = {
|
|
34
32
|
threadId: string;
|
|
33
|
+
isGroup: boolean;
|
|
34
|
+
senderId: string;
|
|
35
|
+
senderName?: string;
|
|
36
|
+
groupName?: string;
|
|
37
|
+
content: string;
|
|
38
|
+
timestampMs: number;
|
|
35
39
|
msgId?: string;
|
|
36
40
|
cliMsgId?: string;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
senderName?: string;
|
|
44
|
-
fromId?: string;
|
|
45
|
-
};
|
|
41
|
+
hasAnyMention?: boolean;
|
|
42
|
+
wasExplicitlyMentioned?: boolean;
|
|
43
|
+
canResolveExplicitMention?: boolean;
|
|
44
|
+
implicitMention?: boolean;
|
|
45
|
+
eventMessage?: ZaloEventMessage;
|
|
46
|
+
raw: unknown;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
export type ZcaUserInfo = {
|
|
@@ -51,28 +52,37 @@ export type ZcaUserInfo = {
|
|
|
51
52
|
avatar?: string;
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
export type
|
|
55
|
+
export type ZaloSendOptions = {
|
|
55
56
|
profile?: string;
|
|
56
|
-
|
|
57
|
+
mediaUrl?: string;
|
|
58
|
+
caption?: string;
|
|
59
|
+
isGroup?: boolean;
|
|
60
|
+
mediaLocalRoots?: readonly string[];
|
|
57
61
|
};
|
|
58
62
|
|
|
59
|
-
export type
|
|
60
|
-
|
|
63
|
+
export type ZaloSendResult = {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
messageId?: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type ZaloGroupContext = {
|
|
70
|
+
groupId: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
members?: string[];
|
|
61
73
|
};
|
|
62
74
|
|
|
63
|
-
export type
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
webhook?: string;
|
|
67
|
-
echo?: boolean;
|
|
68
|
-
prefix?: string;
|
|
75
|
+
export type ZaloAuthStatus = {
|
|
76
|
+
connected: boolean;
|
|
77
|
+
message: string;
|
|
69
78
|
};
|
|
70
79
|
|
|
71
|
-
type ZalouserToolConfig = { allow?: string[]; deny?: string[] };
|
|
80
|
+
export type ZalouserToolConfig = { allow?: string[]; deny?: string[] };
|
|
72
81
|
|
|
73
|
-
type ZalouserGroupConfig = {
|
|
82
|
+
export type ZalouserGroupConfig = {
|
|
74
83
|
allow?: boolean;
|
|
75
84
|
enabled?: boolean;
|
|
85
|
+
requireMention?: boolean;
|
|
76
86
|
tools?: ZalouserToolConfig;
|
|
77
87
|
};
|
|
78
88
|
|