@openclaw/zalouser 2026.3.2 → 2026.3.8-beta.1
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 +24 -0
- package/index.ts +2 -2
- package/package.json +8 -2
- package/src/accounts.test.ts +1 -1
- package/src/accounts.ts +7 -37
- package/src/channel.directory.test.ts +72 -0
- package/src/channel.sendpayload.test.ts +78 -2
- package/src/channel.ts +170 -160
- package/src/config-schema.ts +9 -8
- package/src/monitor.account-scope.test.ts +3 -13
- package/src/monitor.group-gating.test.ts +387 -24
- package/src/monitor.send-mocks.ts +20 -0
- package/src/monitor.ts +444 -151
- package/src/onboarding.ts +19 -52
- package/src/probe.ts +1 -1
- package/src/qr-temp-file.ts +22 -0
- package/src/runtime.ts +5 -13
- package/src/status-issues.ts +1 -1
- package/src/types.ts +3 -0
- package/src/zalo-js.ts +270 -23
- package/src/zca-client.ts +61 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.3.8-beta.1
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.3.8
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.3.7
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
21
|
+
## 2026.3.3
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- Version alignment with core OpenClaw release numbers.
|
|
26
|
+
|
|
3
27
|
## 2026.3.2
|
|
4
28
|
|
|
5
29
|
### Changes
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/zalouser";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalouser";
|
|
3
3
|
import { zalouserDock, zalouserPlugin } from "./src/channel.js";
|
|
4
4
|
import { setZalouserRuntime } from "./src/runtime.js";
|
|
5
5
|
import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/zalouser",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.8-beta.1",
|
|
4
4
|
"description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@sinclair/typebox": "0.34.48",
|
|
8
|
-
"zca-js": "2.1.1"
|
|
8
|
+
"zca-js": "2.1.1",
|
|
9
|
+
"zod": "^4.3.6"
|
|
9
10
|
},
|
|
10
11
|
"openclaw": {
|
|
11
12
|
"extensions": [
|
|
@@ -28,6 +29,11 @@
|
|
|
28
29
|
"npmSpec": "@openclaw/zalouser",
|
|
29
30
|
"localPath": "extensions/zalouser",
|
|
30
31
|
"defaultChoice": "npm"
|
|
32
|
+
},
|
|
33
|
+
"releaseChecks": {
|
|
34
|
+
"rootDependencyMirrorAllowlist": [
|
|
35
|
+
"zca-js"
|
|
36
|
+
]
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
}
|
package/src/accounts.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
1
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/zalouser";
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
getZcaUserInfo,
|
package/src/accounts.ts
CHANGED
|
@@ -1,43 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
normalizeAccountId,
|
|
5
|
-
normalizeOptionalAccountId,
|
|
6
|
-
} from "openclaw/plugin-sdk/account-id";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/zalouser";
|
|
7
3
|
import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
|
|
8
4
|
import { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js";
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return Object.keys(accounts).filter(Boolean);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function listZalouserAccountIds(cfg: OpenClawConfig): string[] {
|
|
19
|
-
const ids = listConfiguredAccountIds(cfg);
|
|
20
|
-
if (ids.length === 0) {
|
|
21
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
22
|
-
}
|
|
23
|
-
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function resolveDefaultZalouserAccountId(cfg: OpenClawConfig): string {
|
|
27
|
-
const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
|
|
28
|
-
const preferred = normalizeOptionalAccountId(zalouserConfig?.defaultAccount);
|
|
29
|
-
if (
|
|
30
|
-
preferred &&
|
|
31
|
-
listZalouserAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
32
|
-
) {
|
|
33
|
-
return preferred;
|
|
34
|
-
}
|
|
35
|
-
const ids = listZalouserAccountIds(cfg);
|
|
36
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
37
|
-
return DEFAULT_ACCOUNT_ID;
|
|
38
|
-
}
|
|
39
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
40
|
-
}
|
|
6
|
+
const {
|
|
7
|
+
listAccountIds: listZalouserAccountIds,
|
|
8
|
+
resolveDefaultAccountId: resolveDefaultZalouserAccountId,
|
|
9
|
+
} = createAccountListHelpers("zalouser");
|
|
10
|
+
export { listZalouserAccountIds, resolveDefaultZalouserAccountId };
|
|
41
11
|
|
|
42
12
|
function resolveAccountConfig(
|
|
43
13
|
cfg: OpenClawConfig,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/zalouser";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const listZaloGroupMembersMock = vi.hoisted(() => vi.fn(async () => []));
|
|
5
|
+
|
|
6
|
+
vi.mock("./zalo-js.js", async (importOriginal) => {
|
|
7
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
listZaloGroupMembers: listZaloGroupMembersMock,
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
vi.mock("./accounts.js", async (importOriginal) => {
|
|
15
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
resolveZalouserAccountSync: () => ({
|
|
19
|
+
accountId: "default",
|
|
20
|
+
profile: "default",
|
|
21
|
+
name: "test",
|
|
22
|
+
enabled: true,
|
|
23
|
+
authenticated: true,
|
|
24
|
+
config: {},
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
import { zalouserPlugin } from "./channel.js";
|
|
30
|
+
|
|
31
|
+
const runtimeStub: RuntimeEnv = {
|
|
32
|
+
log: vi.fn(),
|
|
33
|
+
error: vi.fn(),
|
|
34
|
+
exit: ((code: number): never => {
|
|
35
|
+
throw new Error(`exit ${code}`);
|
|
36
|
+
}) as RuntimeEnv["exit"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("zalouser directory group members", () => {
|
|
40
|
+
it("accepts prefixed group ids from directory groups list output", async () => {
|
|
41
|
+
await zalouserPlugin.directory!.listGroupMembers!({
|
|
42
|
+
cfg: {},
|
|
43
|
+
accountId: "default",
|
|
44
|
+
groupId: "group:1471383327500481391",
|
|
45
|
+
runtime: runtimeStub,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(listZaloGroupMembersMock).toHaveBeenCalledWith("default", "1471383327500481391");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("keeps backward compatibility for raw group ids", async () => {
|
|
52
|
+
await zalouserPlugin.directory!.listGroupMembers!({
|
|
53
|
+
cfg: {},
|
|
54
|
+
accountId: "default",
|
|
55
|
+
groupId: "1471383327500481391",
|
|
56
|
+
runtime: runtimeStub,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(listZaloGroupMembersMock).toHaveBeenCalledWith("default", "1471383327500481391");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("accepts provider-native g- group ids without stripping the prefix", async () => {
|
|
63
|
+
await zalouserPlugin.directory!.listGroupMembers!({
|
|
64
|
+
cfg: {},
|
|
65
|
+
accountId: "default",
|
|
66
|
+
groupId: "g-1471383327500481391",
|
|
67
|
+
runtime: runtimeStub,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(listZaloGroupMembersMock).toHaveBeenCalledWith("default", "g-1471383327500481391");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReplyPayload } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ReplyPayload } from "openclaw/plugin-sdk/zalouser";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { zalouserPlugin } from "./channel.js";
|
|
4
4
|
|
|
@@ -24,7 +24,7 @@ vi.mock("./accounts.js", async (importOriginal) => {
|
|
|
24
24
|
function baseCtx(payload: ReplyPayload) {
|
|
25
25
|
return {
|
|
26
26
|
cfg: {},
|
|
27
|
-
to: "987654321",
|
|
27
|
+
to: "user:987654321",
|
|
28
28
|
text: "",
|
|
29
29
|
payload,
|
|
30
30
|
};
|
|
@@ -49,6 +49,22 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
49
49
|
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-t1" });
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it("group target delegates with isGroup=true and stripped threadId", async () => {
|
|
53
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-g1" });
|
|
54
|
+
|
|
55
|
+
const result = await zalouserPlugin.outbound!.sendPayload!({
|
|
56
|
+
...baseCtx({ text: "hello group" }),
|
|
57
|
+
to: "group:1471383327500481391",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
61
|
+
"1471383327500481391",
|
|
62
|
+
"hello group",
|
|
63
|
+
expect.objectContaining({ isGroup: true }),
|
|
64
|
+
);
|
|
65
|
+
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g1" });
|
|
66
|
+
});
|
|
67
|
+
|
|
52
68
|
it("single media delegates to sendMedia", async () => {
|
|
53
69
|
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-m1" });
|
|
54
70
|
|
|
@@ -64,6 +80,38 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
64
80
|
expect(result).toMatchObject({ channel: "zalouser" });
|
|
65
81
|
});
|
|
66
82
|
|
|
83
|
+
it("treats bare numeric targets as direct chats for backward compatibility", async () => {
|
|
84
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-d1" });
|
|
85
|
+
|
|
86
|
+
const result = await zalouserPlugin.outbound!.sendPayload!({
|
|
87
|
+
...baseCtx({ text: "hello" }),
|
|
88
|
+
to: "987654321",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
92
|
+
"987654321",
|
|
93
|
+
"hello",
|
|
94
|
+
expect.objectContaining({ isGroup: false }),
|
|
95
|
+
);
|
|
96
|
+
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-d1" });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("preserves provider-native group ids when sending to raw g- targets", async () => {
|
|
100
|
+
mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-g-native" });
|
|
101
|
+
|
|
102
|
+
const result = await zalouserPlugin.outbound!.sendPayload!({
|
|
103
|
+
...baseCtx({ text: "hello native group" }),
|
|
104
|
+
to: "g-1471383327500481391",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mockedSend).toHaveBeenCalledWith(
|
|
108
|
+
"g-1471383327500481391",
|
|
109
|
+
"hello native group",
|
|
110
|
+
expect.objectContaining({ isGroup: true }),
|
|
111
|
+
);
|
|
112
|
+
expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g-native" });
|
|
113
|
+
});
|
|
114
|
+
|
|
67
115
|
it("multi-media iterates URLs with caption on first", async () => {
|
|
68
116
|
mockedSend
|
|
69
117
|
.mockResolvedValueOnce({ ok: true, messageId: "zlu-1" })
|
|
@@ -115,3 +163,31 @@ describe("zalouserPlugin outbound sendPayload", () => {
|
|
|
115
163
|
expect(result).toMatchObject({ channel: "zalouser" });
|
|
116
164
|
});
|
|
117
165
|
});
|
|
166
|
+
|
|
167
|
+
describe("zalouserPlugin messaging target normalization", () => {
|
|
168
|
+
it("normalizes user/group aliases to canonical targets", () => {
|
|
169
|
+
const normalize = zalouserPlugin.messaging?.normalizeTarget;
|
|
170
|
+
expect(normalize).toBeTypeOf("function");
|
|
171
|
+
if (!normalize) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
expect(normalize("zlu:g:30003")).toBe("group:30003");
|
|
175
|
+
expect(normalize("zalouser:u:20002")).toBe("user:20002");
|
|
176
|
+
expect(normalize("zlu:g-30003")).toBe("group:g-30003");
|
|
177
|
+
expect(normalize("zalouser:u-20002")).toBe("user:u-20002");
|
|
178
|
+
expect(normalize("20002")).toBe("20002");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("treats canonical and provider-native user/group targets as ids", () => {
|
|
182
|
+
const looksLikeId = zalouserPlugin.messaging?.targetResolver?.looksLikeId;
|
|
183
|
+
expect(looksLikeId).toBeTypeOf("function");
|
|
184
|
+
if (!looksLikeId) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
expect(looksLikeId("user:20002")).toBe(true);
|
|
188
|
+
expect(looksLikeId("group:30003")).toBe(true);
|
|
189
|
+
expect(looksLikeId("g-30003")).toBe(true);
|
|
190
|
+
expect(looksLikeId("u-20002")).toBe(true);
|
|
191
|
+
expect(looksLikeId("Alice Nguyen")).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|