@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 +221 -0
- package/index.ts +28 -0
- package/package.json +14 -0
- package/src/accounts.ts +121 -0
- package/src/channel.test.ts +18 -0
- package/src/channel.ts +523 -0
- package/src/core-bridge.ts +171 -0
- package/src/monitor.ts +372 -0
- package/src/onboarding.ts +312 -0
- package/src/send.ts +150 -0
- package/src/tool.ts +156 -0
- package/src/types.ts +109 -0
- package/src/zca.ts +183 -0
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
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -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
|
+
|