@openclaw/zalouser 2026.1.29

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 ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## 2026.1.29
4
+
5
+ ### Changes
6
+ - Version alignment with core OpenClaw release numbers.
7
+
8
+ ## 2026.1.23
9
+
10
+ ### Changes
11
+ - Version alignment with core OpenClaw release numbers.
12
+
13
+ ## 2026.1.22
14
+
15
+ ### Changes
16
+ - Version alignment with core OpenClaw release numbers.
17
+
18
+ ## 2026.1.21
19
+
20
+ ### Changes
21
+ - Version alignment with core OpenClaw release numbers.
22
+
23
+ ## 2026.1.20
24
+
25
+ ### Changes
26
+ - Version alignment with core OpenClaw release numbers.
27
+
28
+ ## 2026.1.17-1
29
+
30
+ - Initial version with full channel plugin support
31
+ - QR code login via zca-cli
32
+ - Multi-account support
33
+ - Agent tool for sending messages
34
+ - Group and DM policy support
35
+ - ChannelDock for lightweight shared metadata
36
+ - Zod-based config schema validation
37
+ - Setup adapter for programmatic configuration
38
+ - Dedicated probe and status issues modules
package/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @openclaw/zalouser
2
+
3
+ OpenClaw extension for Zalo Personal Account messaging via [zca-cli](https://zca-cli.dev).
4
+
5
+ > **Warning:** Using Zalo automation may result in account suspension or ban. Use at your own risk. This is an unofficial integration.
6
+
7
+ ## Features
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
14
+
15
+ ## Prerequisites
16
+
17
+ Install `zca` CLI and ensure it's in your PATH:
18
+
19
+
20
+ **macOS / Linux:**
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
+ ```powershell
36
+ irm https://get.zca-cli.dev/install.ps1 | iex
37
+
38
+ # Or with custom install directory
39
+ $env:ZCA_INSTALL_DIR = "C:\Tools\zca"; irm https://get.zca-cli.dev/install.ps1 | iex
40
+
41
+ # Install specific version
42
+ iex "& { $(irm https://get.zca-cli.dev/install.ps1) } -Version v1.0.0"
43
+
44
+ # Uninstall
45
+ iex "& { $(irm https://get.zca-cli.dev/install.ps1) } -Uninstall"
46
+ ```
47
+
48
+ ### Manual Download
49
+
50
+ Download binary directly:
51
+
52
+ **macOS / Linux:**
53
+ ```bash
54
+ curl -fsSL https://get.zca-cli.dev/latest/zca-darwin-arm64 -o zca && chmod +x zca
55
+ ```
56
+
57
+ **Windows (PowerShell):**
58
+ ```powershell
59
+ Invoke-WebRequest -Uri https://get.zca-cli.dev/latest/zca-windows-x64.exe -OutFile zca.exe
60
+ ```
61
+
62
+ Available binaries:
63
+ - `zca-darwin-arm64` - macOS Apple Silicon
64
+ - `zca-darwin-x64` - macOS Intel
65
+ - `zca-linux-arm64` - Linux ARM64
66
+ - `zca-linux-x64` - Linux x86_64
67
+ - `zca-windows-x64.exe` - Windows
68
+
69
+ See [zca-cli](https://zca-cli.dev) for manual download (binaries for macOS/Linux/Windows) or building from source.
70
+
71
+ ## Quick Start
72
+
73
+ ### Option 1: Onboarding Wizard (Recommended)
74
+
75
+ ```bash
76
+ openclaw onboard
77
+ # Select "Zalo Personal" from channel list
78
+ # Follow QR code login flow
79
+ ```
80
+
81
+ ### Option 2: Login (QR, on the Gateway machine)
82
+
83
+ ```bash
84
+ openclaw channels login --channel zalouser
85
+ # Scan QR code with Zalo app
86
+ ```
87
+
88
+ ### Send a Message
89
+
90
+ ```bash
91
+ openclaw message send --channel zalouser --target <threadId> --message "Hello from OpenClaw!"
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ After onboarding, your config will include:
97
+
98
+ ```yaml
99
+ channels:
100
+ zalouser:
101
+ enabled: true
102
+ dmPolicy: pairing # pairing | allowlist | open | disabled
103
+ ```
104
+
105
+ For multi-account:
106
+
107
+ ```yaml
108
+ channels:
109
+ zalouser:
110
+ enabled: true
111
+ defaultAccount: default
112
+ accounts:
113
+ default:
114
+ enabled: true
115
+ profile: default
116
+ work:
117
+ enabled: true
118
+ profile: work
119
+ ```
120
+
121
+ ## Commands
122
+
123
+ ### Authentication
124
+
125
+ ```bash
126
+ openclaw channels login --channel zalouser # Login via QR
127
+ openclaw channels login --channel zalouser --account work
128
+ openclaw channels status --probe
129
+ openclaw channels logout --channel zalouser
130
+ ```
131
+
132
+ ### Directory (IDs, contacts, groups)
133
+
134
+ ```bash
135
+ openclaw directory self --channel zalouser
136
+ openclaw directory peers list --channel zalouser --query "name"
137
+ openclaw directory groups list --channel zalouser --query "work"
138
+ openclaw directory groups members --channel zalouser --group-id <id>
139
+ ```
140
+
141
+ ### Account Management
142
+
143
+ ```bash
144
+ zca account list # List all profiles
145
+ zca account current # Show active profile
146
+ zca account switch <profile>
147
+ zca account remove <profile>
148
+ zca account label <profile> "Work Account"
149
+ ```
150
+
151
+ ### Messaging
152
+
153
+ ```bash
154
+ # Text
155
+ openclaw message send --channel zalouser --target <threadId> --message "message"
156
+
157
+ # Media (URL)
158
+ openclaw message send --channel zalouser --target <threadId> --message "caption" --media-url "https://example.com/img.jpg"
159
+ ```
160
+
161
+ ### Listener
162
+
163
+ The listener runs inside the Gateway when the channel is enabled. For debugging,
164
+ use `openclaw channels logs --channel zalouser` or run `zca listen` directly.
165
+
166
+ ### Data Access
167
+
168
+ ```bash
169
+ # Friends
170
+ zca friend list
171
+ zca friend list -j # JSON output
172
+ zca friend find "name"
173
+ zca friend online
174
+
175
+ # Groups
176
+ zca group list
177
+ zca group info <groupId>
178
+ zca group members <groupId>
179
+
180
+ # Profile
181
+ zca me info
182
+ zca me id
183
+ ```
184
+
185
+ ## Multi-Account Support
186
+
187
+ Use `--profile` or `-p` to work with multiple accounts:
188
+
189
+ ```bash
190
+ openclaw channels login --channel zalouser --account work
191
+ openclaw message send --channel zalouser --account work --target <id> --message "Hello"
192
+ ZCA_PROFILE=work zca listen
193
+ ```
194
+
195
+ Profile resolution order: `--profile` flag > `ZCA_PROFILE` env > default
196
+
197
+ ## Agent Tool
198
+
199
+ The extension registers a `zalouser` tool for AI agents:
200
+
201
+ ```json
202
+ {
203
+ "action": "send",
204
+ "threadId": "123456",
205
+ "message": "Hello from AI!",
206
+ "isGroup": false,
207
+ "profile": "default"
208
+ }
209
+ ```
210
+
211
+ Available actions: `send`, `image`, `link`, `friends`, `groups`, `me`, `status`
212
+
213
+ ## Troubleshooting
214
+
215
+ - **Login Issues:** Run `zca auth logout` then `zca auth login`
216
+ - **API Errors:** Try `zca auth cache-refresh` or re-login
217
+ - **File Uploads:** Check size (max 100MB) and path accessibility
218
+
219
+ ## Credits
220
+
221
+ Built on [zca-cli](https://zca-cli.dev) which uses [zca-js](https://github.com/RFS-ADRENO/zca-js).
package/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+
4
+ import { zalouserDock, zalouserPlugin } from "./src/channel.js";
5
+ import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
6
+ import { setZalouserRuntime } from "./src/runtime.js";
7
+
8
+ const plugin = {
9
+ id: "zalouser",
10
+ name: "Zalo Personal",
11
+ description: "Zalo personal account messaging via zca-cli",
12
+ configSchema: emptyPluginConfigSchema(),
13
+ register(api: OpenClawPluginApi) {
14
+ setZalouserRuntime(api.runtime);
15
+ // Register channel plugin (for onboarding & gateway)
16
+ api.registerChannel({ plugin: zalouserPlugin, dock: zalouserDock });
17
+
18
+ // Register agent tool
19
+ api.registerTool({
20
+ name: "zalouser",
21
+ label: "Zalo Personal",
22
+ description:
23
+ "Send messages and access data via Zalo personal account. " +
24
+ "Actions: send (text message), image (send image URL), link (send link), " +
25
+ "friends (list/search friends), groups (list groups), me (profile info), status (auth check).",
26
+ parameters: ZalouserToolSchema,
27
+ execute: executeZalouserTool,
28
+ });
29
+ },
30
+ };
31
+
32
+ export default plugin;
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "zalouser",
3
+ "channels": [
4
+ "zalouser"
5
+ ],
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {}
10
+ }
11
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@openclaw/zalouser",
3
+ "version": "2026.1.29",
4
+ "type": "module",
5
+ "description": "OpenClaw Zalo Personal Account plugin via zca-cli",
6
+ "dependencies": {
7
+ "openclaw": "workspace:*",
8
+ "@sinclair/typebox": "0.34.47"
9
+ },
10
+ "openclaw": {
11
+ "extensions": [
12
+ "./index.ts"
13
+ ],
14
+ "channel": {
15
+ "id": "zalouser",
16
+ "label": "Zalo Personal",
17
+ "selectionLabel": "Zalo (Personal Account)",
18
+ "docsPath": "/channels/zalouser",
19
+ "docsLabel": "zalouser",
20
+ "blurb": "Zalo personal account via QR code login.",
21
+ "aliases": [
22
+ "zlu"
23
+ ],
24
+ "order": 85,
25
+ "quickstartAllowFrom": true
26
+ },
27
+ "install": {
28
+ "npmSpec": "@openclaw/zalouser",
29
+ "localPath": "extensions/zalouser",
30
+ "defaultChoice": "npm"
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,117 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
3
+
4
+ import { runZca, parseJsonOutput } from "./zca.js";
5
+ import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
6
+
7
+ function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
8
+ const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
9
+ if (!accounts || typeof accounts !== "object") return [];
10
+ return Object.keys(accounts).filter(Boolean);
11
+ }
12
+
13
+ export function listZalouserAccountIds(cfg: OpenClawConfig): string[] {
14
+ const ids = listConfiguredAccountIds(cfg);
15
+ if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
16
+ return ids.sort((a, b) => a.localeCompare(b));
17
+ }
18
+
19
+ export function resolveDefaultZalouserAccountId(cfg: OpenClawConfig): string {
20
+ const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
21
+ if (zalouserConfig?.defaultAccount?.trim()) return zalouserConfig.defaultAccount.trim();
22
+ const ids = listZalouserAccountIds(cfg);
23
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
24
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
25
+ }
26
+
27
+ function resolveAccountConfig(
28
+ cfg: OpenClawConfig,
29
+ accountId: string,
30
+ ): ZalouserAccountConfig | undefined {
31
+ const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
32
+ if (!accounts || typeof accounts !== "object") return undefined;
33
+ return accounts[accountId] as ZalouserAccountConfig | undefined;
34
+ }
35
+
36
+ function mergeZalouserAccountConfig(
37
+ cfg: OpenClawConfig,
38
+ accountId: string,
39
+ ): ZalouserAccountConfig {
40
+ const raw = (cfg.channels?.zalouser ?? {}) as ZalouserConfig;
41
+ const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
42
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
43
+ return { ...base, ...account };
44
+ }
45
+
46
+ function resolveZcaProfile(config: ZalouserAccountConfig, accountId: string): string {
47
+ if (config.profile?.trim()) return config.profile.trim();
48
+ if (process.env.ZCA_PROFILE?.trim()) return process.env.ZCA_PROFILE.trim();
49
+ if (accountId !== DEFAULT_ACCOUNT_ID) return accountId;
50
+ return "default";
51
+ }
52
+
53
+ export async function checkZcaAuthenticated(profile: string): Promise<boolean> {
54
+ const result = await runZca(["auth", "status"], { profile, timeout: 5000 });
55
+ return result.ok;
56
+ }
57
+
58
+ export async function resolveZalouserAccount(params: {
59
+ cfg: OpenClawConfig;
60
+ accountId?: string | null;
61
+ }): Promise<ResolvedZalouserAccount> {
62
+ const accountId = normalizeAccountId(params.accountId);
63
+ const baseEnabled = (params.cfg.channels?.zalouser as ZalouserConfig | undefined)?.enabled !== false;
64
+ const merged = mergeZalouserAccountConfig(params.cfg, accountId);
65
+ const accountEnabled = merged.enabled !== false;
66
+ const enabled = baseEnabled && accountEnabled;
67
+ const profile = resolveZcaProfile(merged, accountId);
68
+ const authenticated = await checkZcaAuthenticated(profile);
69
+
70
+ return {
71
+ accountId,
72
+ name: merged.name?.trim() || undefined,
73
+ enabled,
74
+ profile,
75
+ authenticated,
76
+ config: merged,
77
+ };
78
+ }
79
+
80
+ export function resolveZalouserAccountSync(params: {
81
+ cfg: OpenClawConfig;
82
+ accountId?: string | null;
83
+ }): ResolvedZalouserAccount {
84
+ const accountId = normalizeAccountId(params.accountId);
85
+ const baseEnabled = (params.cfg.channels?.zalouser as ZalouserConfig | undefined)?.enabled !== false;
86
+ const merged = mergeZalouserAccountConfig(params.cfg, accountId);
87
+ const accountEnabled = merged.enabled !== false;
88
+ const enabled = baseEnabled && accountEnabled;
89
+ const profile = resolveZcaProfile(merged, accountId);
90
+
91
+ return {
92
+ accountId,
93
+ name: merged.name?.trim() || undefined,
94
+ enabled,
95
+ profile,
96
+ authenticated: false, // unknown without async check
97
+ config: merged,
98
+ };
99
+ }
100
+
101
+ export async function listEnabledZalouserAccounts(
102
+ cfg: OpenClawConfig,
103
+ ): Promise<ResolvedZalouserAccount[]> {
104
+ const ids = listZalouserAccountIds(cfg);
105
+ const accounts = await Promise.all(
106
+ ids.map((accountId) => resolveZalouserAccount({ cfg, accountId }))
107
+ );
108
+ return accounts.filter((account) => account.enabled);
109
+ }
110
+
111
+ export async function getZcaUserInfo(profile: string): Promise<{ userId?: string; displayName?: string } | null> {
112
+ const result = await runZca(["me", "info", "-j"], { profile, timeout: 10000 });
113
+ if (!result.ok) return null;
114
+ return parseJsonOutput<{ userId?: string; displayName?: string }>(result.stdout);
115
+ }
116
+
117
+ export type { ResolvedZalouserAccount } from "./types.js";
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { zalouserPlugin } from "./channel.js";
4
+
5
+ describe("zalouser outbound chunker", () => {
6
+ it("chunks without empty strings and respects limit", () => {
7
+ const chunker = zalouserPlugin.outbound?.chunker;
8
+ expect(chunker).toBeTypeOf("function");
9
+ if (!chunker) return;
10
+
11
+ const limit = 10;
12
+ const chunks = chunker("hello world\nthis is a test", limit);
13
+ expect(chunks.length).toBeGreaterThan(1);
14
+ expect(chunks.every((c) => c.length > 0)).toBe(true);
15
+ expect(chunks.every((c) => c.length <= limit)).toBe(true);
16
+ });
17
+ });