@openclaw/zalouser 2026.3.1 → 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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.2
4
+
5
+ ### Changes
6
+
7
+ - Rebuilt the plugin to use native `zca-js` integration inside OpenClaw (no external `zca` CLI runtime dependency).
8
+
9
+ ### Breaking
10
+
11
+ - **BREAKING:** Removed the old external CLI-based backend (`zca`/`openzca`/`zca-cli`) from runtime flow. Existing setups that depended on external CLI binaries should re-login with `openclaw channels login --channel zalouser` after upgrading.
12
+
3
13
  ## 2026.3.1
4
14
 
5
15
  ### Changes
package/README.md CHANGED
@@ -1,112 +1,78 @@
1
1
  # @openclaw/zalouser
2
2
 
3
- OpenClaw extension for Zalo Personal Account messaging via [zca-cli](https://zca-cli.dev).
3
+ OpenClaw extension for Zalo Personal Account messaging via native `zca-js` integration.
4
4
 
5
5
  > **Warning:** Using Zalo automation may result in account suspension or ban. Use at your own risk. This is an unofficial integration.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Channel Plugin Integration**: Appears in onboarding wizard with QR login
10
- - **Gateway Integration**: Real-time message listening via the gateway
11
- - **Multi-Account Support**: Manage multiple Zalo personal accounts
12
- - **CLI Commands**: Full command-line interface for messaging
13
- - **Agent Tool**: AI agent integration for automated messaging
9
+ - Channel plugin integration with onboarding + QR login
10
+ - In-process listener/sender via `zca-js` (no external CLI)
11
+ - Multi-account support
12
+ - Agent tool integration (`zalouser`)
13
+ - DM/group policy support
14
14
 
15
15
  ## Prerequisites
16
16
 
17
- Install `zca` CLI and ensure it's in your PATH:
17
+ - OpenClaw Gateway
18
+ - Zalo mobile app (for QR login)
18
19
 
19
- **macOS / Linux:**
20
+ No external `zca`, `openzca`, or `zca-cli` binary is required.
20
21
 
21
- ```bash
22
- curl -fsSL https://get.zca-cli.dev/install.sh | bash
23
-
24
- # Or with custom install directory
25
- ZCA_INSTALL_DIR=~/.local/bin curl -fsSL https://get.zca-cli.dev/install.sh | bash
26
-
27
- # Install specific version
28
- curl -fsSL https://get.zca-cli.dev/install.sh | bash -s v1.0.0
29
-
30
- # Uninstall
31
- curl -fsSL https://get.zca-cli.dev/install.sh | bash -s uninstall
32
- ```
33
-
34
- **Windows (PowerShell):**
35
-
36
- ```powershell
37
- irm https://get.zca-cli.dev/install.ps1 | iex
22
+ ## Install
38
23
 
39
- # Or with custom install directory
40
- $env:ZCA_INSTALL_DIR = "C:\Tools\zca"; irm https://get.zca-cli.dev/install.ps1 | iex
41
-
42
- # Install specific version
43
- iex "& { $(irm https://get.zca-cli.dev/install.ps1) } -Version v1.0.0"
44
-
45
- # Uninstall
46
- iex "& { $(irm https://get.zca-cli.dev/install.ps1) } -Uninstall"
47
- ```
48
-
49
- ### Manual Download
50
-
51
- Download binary directly:
52
-
53
- **macOS / Linux:**
24
+ ### Option A: npm
54
25
 
55
26
  ```bash
56
- curl -fsSL https://get.zca-cli.dev/latest/zca-darwin-arm64 -o zca && chmod +x zca
27
+ openclaw plugins install @openclaw/zalouser
57
28
  ```
58
29
 
59
- **Windows (PowerShell):**
30
+ ### Option B: local source checkout
60
31
 
61
- ```powershell
62
- Invoke-WebRequest -Uri https://get.zca-cli.dev/latest/zca-windows-x64.exe -OutFile zca.exe
32
+ ```bash
33
+ openclaw plugins install ./extensions/zalouser
34
+ cd ./extensions/zalouser && pnpm install
63
35
  ```
64
36
 
65
- Available binaries:
37
+ Restart the Gateway after install.
66
38
 
67
- - `zca-darwin-arm64` - macOS Apple Silicon
68
- - `zca-darwin-x64` - macOS Intel
69
- - `zca-linux-arm64` - Linux ARM64
70
- - `zca-linux-x64` - Linux x86_64
71
- - `zca-windows-x64.exe` - Windows
39
+ ## Quick start
72
40
 
73
- See [zca-cli](https://zca-cli.dev) for manual download (binaries for macOS/Linux/Windows) or building from source.
74
-
75
- ## Quick Start
76
-
77
- ### Option 1: Onboarding Wizard (Recommended)
41
+ ### Login (QR)
78
42
 
79
43
  ```bash
80
- openclaw onboard
81
- # Select "Zalo Personal" from channel list
82
- # Follow QR code login flow
44
+ openclaw channels login --channel zalouser
83
45
  ```
84
46
 
85
- ### Option 2: Login (QR, on the Gateway machine)
47
+ Scan the QR code with the Zalo app on your phone.
86
48
 
87
- ```bash
88
- openclaw channels login --channel zalouser
89
- # Scan QR code with Zalo app
49
+ ### Enable channel
50
+
51
+ ```yaml
52
+ channels:
53
+ zalouser:
54
+ enabled: true
55
+ dmPolicy: pairing # pairing | allowlist | open | disabled
90
56
  ```
91
57
 
92
- ### Send a Message
58
+ ### Send a message
93
59
 
94
60
  ```bash
95
- openclaw message send --channel zalouser --target <threadId> --message "Hello from OpenClaw!"
61
+ openclaw message send --channel zalouser --target <threadId> --message "Hello from OpenClaw"
96
62
  ```
97
63
 
98
64
  ## Configuration
99
65
 
100
- After onboarding, your config will include:
66
+ Basic:
101
67
 
102
68
  ```yaml
103
69
  channels:
104
70
  zalouser:
105
71
  enabled: true
106
- dmPolicy: pairing # pairing | allowlist | open | disabled
72
+ dmPolicy: pairing
107
73
  ```
108
74
 
109
- For multi-account:
75
+ Multi-account:
110
76
 
111
77
  ```yaml
112
78
  channels:
@@ -122,104 +88,32 @@ channels:
122
88
  profile: work
123
89
  ```
124
90
 
125
- ## Commands
126
-
127
- ### Authentication
91
+ ## Useful commands
128
92
 
129
93
  ```bash
130
- openclaw channels login --channel zalouser # Login via QR
94
+ openclaw channels login --channel zalouser
131
95
  openclaw channels login --channel zalouser --account work
132
96
  openclaw channels status --probe
133
97
  openclaw channels logout --channel zalouser
134
- ```
135
-
136
- ### Directory (IDs, contacts, groups)
137
98
 
138
- ```bash
139
99
  openclaw directory self --channel zalouser
140
100
  openclaw directory peers list --channel zalouser --query "name"
141
101
  openclaw directory groups list --channel zalouser --query "work"
142
102
  openclaw directory groups members --channel zalouser --group-id <id>
143
103
  ```
144
104
 
145
- ### Account Management
146
-
147
- ```bash
148
- zca account list # List all profiles
149
- zca account current # Show active profile
150
- zca account switch <profile>
151
- zca account remove <profile>
152
- zca account label <profile> "Work Account"
153
- ```
154
-
155
- ### Messaging
156
-
157
- ```bash
158
- # Text
159
- openclaw message send --channel zalouser --target <threadId> --message "message"
160
-
161
- # Media (URL)
162
- openclaw message send --channel zalouser --target <threadId> --message "caption" --media-url "https://example.com/img.jpg"
163
- ```
164
-
165
- ### Listener
166
-
167
- The listener runs inside the Gateway when the channel is enabled. For debugging,
168
- use `openclaw channels logs --channel zalouser` or run `zca listen` directly.
169
-
170
- ### Data Access
105
+ ## Agent tool
171
106
 
172
- ```bash
173
- # Friends
174
- zca friend list
175
- zca friend list -j # JSON output
176
- zca friend find "name"
177
- zca friend online
178
-
179
- # Groups
180
- zca group list
181
- zca group info <groupId>
182
- zca group members <groupId>
183
-
184
- # Profile
185
- zca me info
186
- zca me id
187
- ```
188
-
189
- ## Multi-Account Support
190
-
191
- Use `--profile` or `-p` to work with multiple accounts:
192
-
193
- ```bash
194
- openclaw channels login --channel zalouser --account work
195
- openclaw message send --channel zalouser --account work --target <id> --message "Hello"
196
- ZCA_PROFILE=work zca listen
197
- ```
198
-
199
- Profile resolution order: `--profile` flag > `ZCA_PROFILE` env > default
200
-
201
- ## Agent Tool
202
-
203
- The extension registers a `zalouser` tool for AI agents:
204
-
205
- ```json
206
- {
207
- "action": "send",
208
- "threadId": "123456",
209
- "message": "Hello from AI!",
210
- "isGroup": false,
211
- "profile": "default"
212
- }
213
- ```
107
+ The extension registers a `zalouser` tool for AI agents.
214
108
 
215
109
  Available actions: `send`, `image`, `link`, `friends`, `groups`, `me`, `status`
216
110
 
217
111
  ## Troubleshooting
218
112
 
219
- - **Login Issues:** Run `zca auth logout` then `zca auth login`
220
- - **API Errors:** Try `zca auth cache-refresh` or re-login
221
- - **File Uploads:** Check size (max 100MB) and path accessibility
113
+ - Login not persisted: `openclaw channels logout --channel zalouser && openclaw channels login --channel zalouser`
114
+ - Probe status: `openclaw channels status --probe`
115
+ - Name resolution issues (allowlist/groups): use numeric IDs or exact Zalo names
222
116
 
223
117
  ## Credits
224
118
 
225
- Built on [zca-cli](https://zca-cli.dev) which uses [zca-js](https://github.com/RFS-ADRENO/zca-js).
119
+ Built on [zca-js](https://github.com/RFS-ADRENO/zca-js).
package/index.ts CHANGED
@@ -7,14 +7,12 @@ import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
7
7
  const plugin = {
8
8
  id: "zalouser",
9
9
  name: "Zalo Personal",
10
- description: "Zalo personal account messaging via zca-cli",
10
+ description: "Zalo personal account messaging via native zca-js integration",
11
11
  configSchema: emptyPluginConfigSchema(),
12
12
  register(api: OpenClawPluginApi) {
13
13
  setZalouserRuntime(api.runtime);
14
- // Register channel plugin (for onboarding & gateway)
15
14
  api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock });
16
15
 
17
- // Register agent tool
18
16
  api.registerTool({
19
17
  name: "zalouser",
20
18
  label: "Zalo Personal",
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@openclaw/zalouser",
3
- "version": "2026.3.1",
4
- "description": "OpenClaw Zalo Personal Account plugin via zca-cli",
3
+ "version": "2026.3.2",
4
+ "description": "OpenClaw Zalo Personal Account plugin via native zca-js integration",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@sinclair/typebox": "0.34.48"
7
+ "@sinclair/typebox": "0.34.48",
8
+ "zca-js": "2.1.1"
8
9
  },
9
10
  "openclaw": {
10
11
  "extensions": [
@@ -0,0 +1,214 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ getZcaUserInfo,
6
+ listEnabledZalouserAccounts,
7
+ listZalouserAccountIds,
8
+ resolveDefaultZalouserAccountId,
9
+ resolveZalouserAccount,
10
+ resolveZalouserAccountSync,
11
+ } from "./accounts.js";
12
+ import { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js";
13
+
14
+ vi.mock("./zalo-js.js", () => ({
15
+ checkZaloAuthenticated: vi.fn(),
16
+ getZaloUserInfo: vi.fn(),
17
+ }));
18
+
19
+ const mockCheckAuthenticated = vi.mocked(checkZaloAuthenticated);
20
+ const mockGetUserInfo = vi.mocked(getZaloUserInfo);
21
+
22
+ function asConfig(value: unknown): OpenClawConfig {
23
+ return value as OpenClawConfig;
24
+ }
25
+
26
+ describe("zalouser account resolution", () => {
27
+ beforeEach(() => {
28
+ mockCheckAuthenticated.mockReset();
29
+ mockGetUserInfo.mockReset();
30
+ delete process.env.ZALOUSER_PROFILE;
31
+ delete process.env.ZCA_PROFILE;
32
+ });
33
+
34
+ it("returns default account id when no accounts are configured", () => {
35
+ expect(listZalouserAccountIds(asConfig({}))).toEqual([DEFAULT_ACCOUNT_ID]);
36
+ });
37
+
38
+ it("returns sorted configured account ids", () => {
39
+ const cfg = asConfig({
40
+ channels: {
41
+ zalouser: {
42
+ accounts: {
43
+ work: {},
44
+ personal: {},
45
+ default: {},
46
+ },
47
+ },
48
+ },
49
+ });
50
+
51
+ expect(listZalouserAccountIds(cfg)).toEqual(["default", "personal", "work"]);
52
+ });
53
+
54
+ it("uses configured defaultAccount when present", () => {
55
+ const cfg = asConfig({
56
+ channels: {
57
+ zalouser: {
58
+ defaultAccount: "work",
59
+ accounts: {
60
+ default: {},
61
+ work: {},
62
+ },
63
+ },
64
+ },
65
+ });
66
+
67
+ expect(resolveDefaultZalouserAccountId(cfg)).toBe("work");
68
+ });
69
+
70
+ it("falls back to default account when configured defaultAccount is missing", () => {
71
+ const cfg = asConfig({
72
+ channels: {
73
+ zalouser: {
74
+ defaultAccount: "missing",
75
+ accounts: {
76
+ default: {},
77
+ work: {},
78
+ },
79
+ },
80
+ },
81
+ });
82
+
83
+ expect(resolveDefaultZalouserAccountId(cfg)).toBe("default");
84
+ });
85
+
86
+ it("falls back to first sorted configured account when default is absent", () => {
87
+ const cfg = asConfig({
88
+ channels: {
89
+ zalouser: {
90
+ accounts: {
91
+ zzz: {},
92
+ aaa: {},
93
+ },
94
+ },
95
+ },
96
+ });
97
+
98
+ expect(resolveDefaultZalouserAccountId(cfg)).toBe("aaa");
99
+ });
100
+
101
+ it("resolves sync account by merging base + account config", () => {
102
+ const cfg = asConfig({
103
+ channels: {
104
+ zalouser: {
105
+ enabled: true,
106
+ dmPolicy: "pairing",
107
+ accounts: {
108
+ work: {
109
+ enabled: false,
110
+ name: "Work",
111
+ dmPolicy: "allowlist",
112
+ allowFrom: ["123"],
113
+ },
114
+ },
115
+ },
116
+ },
117
+ });
118
+
119
+ const resolved = resolveZalouserAccountSync({ cfg, accountId: "work" });
120
+ expect(resolved.accountId).toBe("work");
121
+ expect(resolved.enabled).toBe(false);
122
+ expect(resolved.name).toBe("Work");
123
+ expect(resolved.config.dmPolicy).toBe("allowlist");
124
+ expect(resolved.config.allowFrom).toEqual(["123"]);
125
+ });
126
+
127
+ it("resolves profile precedence correctly", () => {
128
+ const cfg = asConfig({
129
+ channels: {
130
+ zalouser: {
131
+ accounts: {
132
+ work: {},
133
+ },
134
+ },
135
+ },
136
+ });
137
+
138
+ process.env.ZALOUSER_PROFILE = "zalo-env";
139
+ expect(resolveZalouserAccountSync({ cfg, accountId: "work" }).profile).toBe("zalo-env");
140
+
141
+ delete process.env.ZALOUSER_PROFILE;
142
+ process.env.ZCA_PROFILE = "zca-env";
143
+ expect(resolveZalouserAccountSync({ cfg, accountId: "work" }).profile).toBe("zca-env");
144
+
145
+ delete process.env.ZCA_PROFILE;
146
+ expect(resolveZalouserAccountSync({ cfg, accountId: "work" }).profile).toBe("work");
147
+ });
148
+
149
+ it("uses explicit profile from config over env fallback", () => {
150
+ process.env.ZALOUSER_PROFILE = "env-profile";
151
+ const cfg = asConfig({
152
+ channels: {
153
+ zalouser: {
154
+ accounts: {
155
+ work: {
156
+ profile: "explicit-profile",
157
+ },
158
+ },
159
+ },
160
+ },
161
+ });
162
+
163
+ expect(resolveZalouserAccountSync({ cfg, accountId: "work" }).profile).toBe("explicit-profile");
164
+ });
165
+
166
+ it("checks authentication during async account resolution", async () => {
167
+ mockCheckAuthenticated.mockResolvedValueOnce(true);
168
+ const cfg = asConfig({
169
+ channels: {
170
+ zalouser: {
171
+ accounts: {
172
+ default: {},
173
+ },
174
+ },
175
+ },
176
+ });
177
+
178
+ const resolved = await resolveZalouserAccount({ cfg, accountId: "default" });
179
+ expect(mockCheckAuthenticated).toHaveBeenCalledWith("default");
180
+ expect(resolved.authenticated).toBe(true);
181
+ });
182
+
183
+ it("filters disabled accounts when listing enabled accounts", async () => {
184
+ mockCheckAuthenticated.mockResolvedValue(true);
185
+ const cfg = asConfig({
186
+ channels: {
187
+ zalouser: {
188
+ accounts: {
189
+ default: { enabled: true },
190
+ work: { enabled: false },
191
+ },
192
+ },
193
+ },
194
+ });
195
+
196
+ const accounts = await listEnabledZalouserAccounts(cfg);
197
+ expect(accounts.map((account) => account.accountId)).toEqual(["default"]);
198
+ });
199
+
200
+ it("maps account info helper from zalo-js", async () => {
201
+ mockGetUserInfo.mockResolvedValueOnce({
202
+ userId: "123",
203
+ displayName: "Alice",
204
+ avatar: "https://example.com/avatar.png",
205
+ });
206
+ expect(await getZcaUserInfo("default")).toEqual({
207
+ userId: "123",
208
+ displayName: "Alice",
209
+ });
210
+
211
+ mockGetUserInfo.mockResolvedValueOnce(null);
212
+ expect(await getZcaUserInfo("default")).toBeNull();
213
+ });
214
+ });
package/src/accounts.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  normalizeOptionalAccountId,
6
6
  } from "openclaw/plugin-sdk/account-id";
7
7
  import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
8
- import { runZca, parseJsonOutput } from "./zca.js";
8
+ import { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js";
9
9
 
10
10
  function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
11
11
  const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
@@ -57,10 +57,13 @@ function mergeZalouserAccountConfig(cfg: OpenClawConfig, accountId: string): Zal
57
57
  return { ...base, ...account };
58
58
  }
59
59
 
60
- function resolveZcaProfile(config: ZalouserAccountConfig, accountId: string): string {
60
+ function resolveProfile(config: ZalouserAccountConfig, accountId: string): string {
61
61
  if (config.profile?.trim()) {
62
62
  return config.profile.trim();
63
63
  }
64
+ if (process.env.ZALOUSER_PROFILE?.trim()) {
65
+ return process.env.ZALOUSER_PROFILE.trim();
66
+ }
64
67
  if (process.env.ZCA_PROFILE?.trim()) {
65
68
  return process.env.ZCA_PROFILE.trim();
66
69
  }
@@ -70,11 +73,6 @@ function resolveZcaProfile(config: ZalouserAccountConfig, accountId: string): st
70
73
  return "default";
71
74
  }
72
75
 
73
- export async function checkZcaAuthenticated(profile: string): Promise<boolean> {
74
- const result = await runZca(["auth", "status"], { profile, timeout: 5000 });
75
- return result.ok;
76
- }
77
-
78
76
  export async function resolveZalouserAccount(params: {
79
77
  cfg: OpenClawConfig;
80
78
  accountId?: string | null;
@@ -85,8 +83,8 @@ export async function resolveZalouserAccount(params: {
85
83
  const merged = mergeZalouserAccountConfig(params.cfg, accountId);
86
84
  const accountEnabled = merged.enabled !== false;
87
85
  const enabled = baseEnabled && accountEnabled;
88
- const profile = resolveZcaProfile(merged, accountId);
89
- const authenticated = await checkZcaAuthenticated(profile);
86
+ const profile = resolveProfile(merged, accountId);
87
+ const authenticated = await checkZaloAuthenticated(profile);
90
88
 
91
89
  return {
92
90
  accountId,
@@ -108,14 +106,14 @@ export function resolveZalouserAccountSync(params: {
108
106
  const merged = mergeZalouserAccountConfig(params.cfg, accountId);
109
107
  const accountEnabled = merged.enabled !== false;
110
108
  const enabled = baseEnabled && accountEnabled;
111
- const profile = resolveZcaProfile(merged, accountId);
109
+ const profile = resolveProfile(merged, accountId);
112
110
 
113
111
  return {
114
112
  accountId,
115
113
  name: merged.name?.trim() || undefined,
116
114
  enabled,
117
115
  profile,
118
- authenticated: false, // unknown without async check
116
+ authenticated: false,
119
117
  config: merged,
120
118
  };
121
119
  }
@@ -133,11 +131,16 @@ export async function listEnabledZalouserAccounts(
133
131
  export async function getZcaUserInfo(
134
132
  profile: string,
135
133
  ): Promise<{ userId?: string; displayName?: string } | null> {
136
- const result = await runZca(["me", "info", "-j"], { profile, timeout: 10000 });
137
- if (!result.ok) {
134
+ const info = await getZaloUserInfo(profile);
135
+ if (!info) {
138
136
  return null;
139
137
  }
140
- return parseJsonOutput<{ userId?: string; displayName?: string }>(result.stdout);
138
+ return {
139
+ userId: info.userId,
140
+ displayName: info.displayName,
141
+ };
141
142
  }
142
143
 
144
+ export { checkZaloAuthenticated as checkZcaAuthenticated };
145
+
143
146
  export type { ResolvedZalouserAccount } from "./types.js";