@clawdbot/zalouser 2026.1.16

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/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @clawdbot/zalouser
2
+
3
+ Clawdbot 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
+ clawdbot 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
+ clawdbot channels login --channel zalouser
85
+ # Scan QR code with Zalo app
86
+ ```
87
+
88
+ ### Send a Message
89
+
90
+ ```bash
91
+ clawdbot message send --channel zalouser --target <threadId> --message "Hello from Clawdbot!"
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
+ clawdbot channels login --channel zalouser # Login via QR
127
+ clawdbot channels login --channel zalouser --account work
128
+ clawdbot channels status --probe
129
+ clawdbot channels logout --channel zalouser
130
+ ```
131
+
132
+ ### Directory (IDs, contacts, groups)
133
+
134
+ ```bash
135
+ clawdbot directory self --channel zalouser
136
+ clawdbot directory peers list --channel zalouser --query "name"
137
+ clawdbot directory groups list --channel zalouser --query "work"
138
+ clawdbot 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
+ clawdbot message send --channel zalouser --target <threadId> --message "message"
156
+
157
+ # Media (URL)
158
+ clawdbot 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 `clawdbot 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
+ clawdbot channels login --channel zalouser --account work
191
+ clawdbot 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,28 @@
1
+ import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
2
+
3
+ import { zalouserPlugin } from "./src/channel.js";
4
+ import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
5
+
6
+ const plugin = {
7
+ id: "zalouser",
8
+ name: "Zalo Personal",
9
+ description: "Zalo personal account messaging via zca-cli",
10
+ register(api: ClawdbotPluginApi) {
11
+ // Register channel plugin (for onboarding & gateway)
12
+ api.registerChannel(zalouserPlugin);
13
+
14
+ // Register agent tool
15
+ api.registerTool({
16
+ name: "zalouser",
17
+ label: "Zalo Personal",
18
+ description:
19
+ "Send messages and access data via Zalo personal account. " +
20
+ "Actions: send (text message), image (send image URL), link (send link), " +
21
+ "friends (list/search friends), groups (list groups), me (profile info), status (auth check).",
22
+ parameters: ZalouserToolSchema,
23
+ execute: executeZalouserTool,
24
+ });
25
+ },
26
+ };
27
+
28
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@clawdbot/zalouser",
3
+ "version": "2026.1.16",
4
+ "type": "module",
5
+ "description": "Clawdbot Zalo Personal Account plugin via zca-cli",
6
+ "dependencies": {
7
+ "@sinclair/typebox": "0.34.47"
8
+ },
9
+ "clawdbot": {
10
+ "extensions": [
11
+ "./index.ts"
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,121 @@
1
+ import { runZca, parseJsonOutput } from "./zca.js";
2
+ import {
3
+ DEFAULT_ACCOUNT_ID,
4
+ type CoreConfig,
5
+ type ResolvedZalouserAccount,
6
+ type ZalouserAccountConfig,
7
+ type ZalouserConfig,
8
+ } from "./types.js";
9
+
10
+ function listConfiguredAccountIds(cfg: CoreConfig): string[] {
11
+ const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
12
+ if (!accounts || typeof accounts !== "object") return [];
13
+ return Object.keys(accounts).filter(Boolean);
14
+ }
15
+
16
+ export function listZalouserAccountIds(cfg: CoreConfig): string[] {
17
+ const ids = listConfiguredAccountIds(cfg);
18
+ if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
19
+ return ids.sort((a, b) => a.localeCompare(b));
20
+ }
21
+
22
+ export function resolveDefaultZalouserAccountId(cfg: CoreConfig): string {
23
+ const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
24
+ if (zalouserConfig?.defaultAccount?.trim()) return zalouserConfig.defaultAccount.trim();
25
+ const ids = listZalouserAccountIds(cfg);
26
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
27
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
28
+ }
29
+
30
+ export function normalizeAccountId(accountId?: string | null): string {
31
+ const trimmed = accountId?.trim();
32
+ if (!trimmed) return DEFAULT_ACCOUNT_ID;
33
+ return trimmed.toLowerCase();
34
+ }
35
+
36
+ function resolveAccountConfig(
37
+ cfg: CoreConfig,
38
+ accountId: string,
39
+ ): ZalouserAccountConfig | undefined {
40
+ const accounts = (cfg.channels?.zalouser as ZalouserConfig | undefined)?.accounts;
41
+ if (!accounts || typeof accounts !== "object") return undefined;
42
+ return accounts[accountId] as ZalouserAccountConfig | undefined;
43
+ }
44
+
45
+ function mergeZalouserAccountConfig(cfg: CoreConfig, accountId: string): ZalouserAccountConfig {
46
+ const raw = (cfg.channels?.zalouser ?? {}) as ZalouserConfig;
47
+ const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
48
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
49
+ return { ...base, ...account };
50
+ }
51
+
52
+ function resolveZcaProfile(config: ZalouserAccountConfig, accountId: string): string {
53
+ if (config.profile?.trim()) return config.profile.trim();
54
+ if (process.env.ZCA_PROFILE?.trim()) return process.env.ZCA_PROFILE.trim();
55
+ if (accountId !== DEFAULT_ACCOUNT_ID) return accountId;
56
+ return "default";
57
+ }
58
+
59
+ export async function checkZcaAuthenticated(profile: string): Promise<boolean> {
60
+ const result = await runZca(["auth", "status"], { profile, timeout: 5000 });
61
+ return result.ok;
62
+ }
63
+
64
+ export async function resolveZalouserAccount(params: {
65
+ cfg: CoreConfig;
66
+ accountId?: string | null;
67
+ }): Promise<ResolvedZalouserAccount> {
68
+ const accountId = normalizeAccountId(params.accountId);
69
+ const baseEnabled = (params.cfg.channels?.zalouser as ZalouserConfig | undefined)?.enabled !== false;
70
+ const merged = mergeZalouserAccountConfig(params.cfg, accountId);
71
+ const accountEnabled = merged.enabled !== false;
72
+ const enabled = baseEnabled && accountEnabled;
73
+ const profile = resolveZcaProfile(merged, accountId);
74
+ const authenticated = await checkZcaAuthenticated(profile);
75
+
76
+ return {
77
+ accountId,
78
+ name: merged.name?.trim() || undefined,
79
+ enabled,
80
+ profile,
81
+ authenticated,
82
+ config: merged,
83
+ };
84
+ }
85
+
86
+ export function resolveZalouserAccountSync(params: {
87
+ cfg: CoreConfig;
88
+ accountId?: string | null;
89
+ }): ResolvedZalouserAccount {
90
+ const accountId = normalizeAccountId(params.accountId);
91
+ const baseEnabled = (params.cfg.channels?.zalouser as ZalouserConfig | undefined)?.enabled !== false;
92
+ const merged = mergeZalouserAccountConfig(params.cfg, accountId);
93
+ const accountEnabled = merged.enabled !== false;
94
+ const enabled = baseEnabled && accountEnabled;
95
+ const profile = resolveZcaProfile(merged, accountId);
96
+
97
+ return {
98
+ accountId,
99
+ name: merged.name?.trim() || undefined,
100
+ enabled,
101
+ profile,
102
+ authenticated: false, // unknown without async check
103
+ config: merged,
104
+ };
105
+ }
106
+
107
+ export async function listEnabledZalouserAccounts(cfg: CoreConfig): Promise<ResolvedZalouserAccount[]> {
108
+ const ids = listZalouserAccountIds(cfg);
109
+ const accounts = await Promise.all(
110
+ ids.map((accountId) => resolveZalouserAccount({ cfg, accountId }))
111
+ );
112
+ return accounts.filter((account) => account.enabled);
113
+ }
114
+
115
+ export async function getZcaUserInfo(profile: string): Promise<{ userId?: string; displayName?: string } | null> {
116
+ const result = await runZca(["me", "info", "-j"], { profile, timeout: 10000 });
117
+ if (!result.ok) return null;
118
+ return parseJsonOutput<{ userId?: string; displayName?: string }>(result.stdout);
119
+ }
120
+
121
+ export type { ResolvedZalouserAccount } from "./types.js";
@@ -0,0 +1,18 @@
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
+ });
18
+