@openclaw/zalo 2026.1.29 → 2026.2.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 +29 -0
- package/README.md +6 -6
- package/index.ts +0 -1
- package/openclaw.plugin.json +1 -3
- package/package.json +9 -6
- package/src/accounts.ts +16 -7
- package/src/actions.ts +11 -6
- package/src/api.ts +3 -1
- package/src/channel.directory.test.ts +15 -7
- package/src/channel.ts +53 -33
- package/src/monitor.ts +31 -38
- package/src/monitor.webhook.test.ts +24 -21
- package/src/onboarding.ts +39 -43
- package/src/proxy.ts +8 -5
- package/src/send.ts +18 -11
- package/src/status-issues.ts +11 -8
- package/src/token.ts +15 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,58 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.2.2
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.1.31
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.1.30
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.1.29
|
|
4
22
|
|
|
5
23
|
### Changes
|
|
24
|
+
|
|
6
25
|
- Version alignment with core OpenClaw release numbers.
|
|
7
26
|
|
|
8
27
|
## 2026.1.23
|
|
9
28
|
|
|
10
29
|
### Changes
|
|
30
|
+
|
|
11
31
|
- Version alignment with core OpenClaw release numbers.
|
|
12
32
|
|
|
13
33
|
## 2026.1.22
|
|
14
34
|
|
|
15
35
|
### Changes
|
|
36
|
+
|
|
16
37
|
- Version alignment with core OpenClaw release numbers.
|
|
17
38
|
|
|
18
39
|
## 2026.1.21
|
|
19
40
|
|
|
20
41
|
### Changes
|
|
42
|
+
|
|
21
43
|
- Version alignment with core OpenClaw release numbers.
|
|
22
44
|
|
|
23
45
|
## 2026.1.20
|
|
24
46
|
|
|
25
47
|
### Changes
|
|
48
|
+
|
|
26
49
|
- Version alignment with core OpenClaw release numbers.
|
|
27
50
|
|
|
28
51
|
## 2026.1.17-1
|
|
29
52
|
|
|
30
53
|
### Changes
|
|
54
|
+
|
|
31
55
|
- Version alignment with core OpenClaw release numbers.
|
|
32
56
|
|
|
33
57
|
## 2026.1.17
|
|
34
58
|
|
|
35
59
|
### Changes
|
|
60
|
+
|
|
36
61
|
- Version alignment with core OpenClaw release numbers.
|
|
37
62
|
|
|
38
63
|
## 2026.1.16
|
|
39
64
|
|
|
40
65
|
### Changes
|
|
66
|
+
|
|
41
67
|
- Version alignment with core OpenClaw release numbers.
|
|
42
68
|
|
|
43
69
|
## 2026.1.15
|
|
44
70
|
|
|
45
71
|
### Changes
|
|
72
|
+
|
|
46
73
|
- Version alignment with core OpenClaw release numbers.
|
|
47
74
|
|
|
48
75
|
## 2026.1.14
|
|
49
76
|
|
|
50
77
|
### Changes
|
|
78
|
+
|
|
51
79
|
- Version alignment with core OpenClaw release numbers.
|
|
52
80
|
|
|
53
81
|
## 0.1.0
|
|
54
82
|
|
|
55
83
|
### Features
|
|
84
|
+
|
|
56
85
|
- Zalo Bot API channel plugin with token-based auth (env/config/file).
|
|
57
86
|
- Direct message support (DMs only) with pairing/allowlist/open/disabled policies.
|
|
58
87
|
- Polling and webhook delivery modes.
|
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ Onboarding: select Zalo and confirm the install prompt to fetch the plugin autom
|
|
|
25
25
|
enabled: true,
|
|
26
26
|
botToken: "12345689:abc-xyz",
|
|
27
27
|
dmPolicy: "pairing",
|
|
28
|
-
proxy: "http://proxy.local:8080"
|
|
29
|
-
}
|
|
30
|
-
}
|
|
28
|
+
proxy: "http://proxy.local:8080",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -39,9 +39,9 @@ Onboarding: select Zalo and confirm the install prompt to fetch the plugin autom
|
|
|
39
39
|
zalo: {
|
|
40
40
|
webhookUrl: "https://example.com/zalo-webhook",
|
|
41
41
|
webhookSecret: "your-secret-8-plus-chars",
|
|
42
|
-
webhookPath: "/zalo-webhook"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
42
|
+
webhookPath: "/zalo-webhook",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
package/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
-
|
|
4
3
|
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
|
5
4
|
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
|
6
5
|
import { setZaloRuntime } from "./src/runtime.js";
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/zalo",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2026.2.2",
|
|
5
4
|
"description": "OpenClaw Zalo channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"openclaw": "workspace:*",
|
|
8
|
+
"undici": "7.20.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"openclaw": "workspace:*"
|
|
12
|
+
},
|
|
6
13
|
"openclaw": {
|
|
7
14
|
"extensions": [
|
|
8
15
|
"./index.ts"
|
|
@@ -25,9 +32,5 @@
|
|
|
25
32
|
"localPath": "extensions/zalo",
|
|
26
33
|
"defaultChoice": "npm"
|
|
27
34
|
}
|
|
28
|
-
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"openclaw": "workspace:*",
|
|
31
|
-
"undici": "7.19.0"
|
|
32
35
|
}
|
|
33
36
|
}
|
package/src/accounts.ts
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
|
3
|
-
|
|
4
3
|
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
|
|
5
4
|
import { resolveZaloToken } from "./token.js";
|
|
6
5
|
|
|
7
6
|
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
|
8
7
|
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
|
|
9
|
-
if (!accounts || typeof accounts !== "object")
|
|
8
|
+
if (!accounts || typeof accounts !== "object") {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
10
11
|
return Object.keys(accounts).filter(Boolean);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function listZaloAccountIds(cfg: OpenClawConfig): string[] {
|
|
14
15
|
const ids = listConfiguredAccountIds(cfg);
|
|
15
|
-
if (ids.length === 0)
|
|
16
|
-
|
|
16
|
+
if (ids.length === 0) {
|
|
17
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
18
|
+
}
|
|
19
|
+
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string {
|
|
20
23
|
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
|
21
|
-
if (zaloConfig?.defaultAccount?.trim())
|
|
24
|
+
if (zaloConfig?.defaultAccount?.trim()) {
|
|
25
|
+
return zaloConfig.defaultAccount.trim();
|
|
26
|
+
}
|
|
22
27
|
const ids = listZaloAccountIds(cfg);
|
|
23
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID))
|
|
28
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
29
|
+
return DEFAULT_ACCOUNT_ID;
|
|
30
|
+
}
|
|
24
31
|
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -29,7 +36,9 @@ function resolveAccountConfig(
|
|
|
29
36
|
accountId: string,
|
|
30
37
|
): ZaloAccountConfig | undefined {
|
|
31
38
|
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
|
|
32
|
-
if (!accounts || typeof accounts !== "object")
|
|
39
|
+
if (!accounts || typeof accounts !== "object") {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
33
42
|
return accounts[accountId] as ZaloAccountConfig | undefined;
|
|
34
43
|
}
|
|
35
44
|
|
package/src/actions.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
OpenClawConfig,
|
|
5
5
|
} from "openclaw/plugin-sdk";
|
|
6
6
|
import { jsonResult, readStringParam } from "openclaw/plugin-sdk";
|
|
7
|
-
|
|
8
7
|
import { listEnabledZaloAccounts } from "./accounts.js";
|
|
9
8
|
import { sendMessageZalo } from "./send.js";
|
|
10
9
|
|
|
@@ -18,17 +17,23 @@ function listEnabledAccounts(cfg: OpenClawConfig) {
|
|
|
18
17
|
|
|
19
18
|
export const zaloMessageActions: ChannelMessageActionAdapter = {
|
|
20
19
|
listActions: ({ cfg }) => {
|
|
21
|
-
const accounts = listEnabledAccounts(cfg
|
|
22
|
-
if (accounts.length === 0)
|
|
20
|
+
const accounts = listEnabledAccounts(cfg);
|
|
21
|
+
if (accounts.length === 0) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
23
24
|
const actions = new Set<ChannelMessageActionName>(["send"]);
|
|
24
25
|
return Array.from(actions);
|
|
25
26
|
},
|
|
26
27
|
supportsButtons: () => false,
|
|
27
28
|
extractToolSend: ({ args }) => {
|
|
28
29
|
const action = typeof args.action === "string" ? args.action.trim() : "";
|
|
29
|
-
if (action !== "sendMessage")
|
|
30
|
+
if (action !== "sendMessage") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
30
33
|
const to = typeof args.to === "string" ? args.to : undefined;
|
|
31
|
-
if (!to)
|
|
34
|
+
if (!to) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
32
37
|
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
|
33
38
|
return { to, accountId };
|
|
34
39
|
},
|
|
@@ -44,7 +49,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = {
|
|
|
44
49
|
const result = await sendMessageZalo(to ?? "", content ?? "", {
|
|
45
50
|
accountId: accountId ?? undefined,
|
|
46
51
|
mediaUrl: mediaUrl ?? undefined,
|
|
47
|
-
cfg: cfg
|
|
52
|
+
cfg: cfg,
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
if (!result.ok) {
|
package/src/api.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
|
-
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
5
3
|
import { zaloPlugin } from "./channel.js";
|
|
6
4
|
|
|
7
5
|
describe("zalo directory", () => {
|
|
@@ -19,7 +17,12 @@ describe("zalo directory", () => {
|
|
|
19
17
|
expect(zaloPlugin.directory?.listGroups).toBeTruthy();
|
|
20
18
|
|
|
21
19
|
await expect(
|
|
22
|
-
zaloPlugin.directory!.listPeers({
|
|
20
|
+
zaloPlugin.directory!.listPeers({
|
|
21
|
+
cfg,
|
|
22
|
+
accountId: undefined,
|
|
23
|
+
query: undefined,
|
|
24
|
+
limit: undefined,
|
|
25
|
+
}),
|
|
23
26
|
).resolves.toEqual(
|
|
24
27
|
expect.arrayContaining([
|
|
25
28
|
{ kind: "user", id: "123" },
|
|
@@ -28,8 +31,13 @@ describe("zalo directory", () => {
|
|
|
28
31
|
]),
|
|
29
32
|
);
|
|
30
33
|
|
|
31
|
-
await expect(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
await expect(
|
|
35
|
+
zaloPlugin.directory!.listGroups({
|
|
36
|
+
cfg,
|
|
37
|
+
accountId: undefined,
|
|
38
|
+
query: undefined,
|
|
39
|
+
limit: undefined,
|
|
40
|
+
}),
|
|
41
|
+
).resolves.toEqual([]);
|
|
34
42
|
});
|
|
35
43
|
});
|
package/src/channel.ts
CHANGED
|
@@ -15,13 +15,17 @@ import {
|
|
|
15
15
|
PAIRING_APPROVED_MESSAGE,
|
|
16
16
|
setAccountEnabledInConfigSection,
|
|
17
17
|
} from "openclaw/plugin-sdk";
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
listZaloAccountIds,
|
|
20
|
+
resolveDefaultZaloAccountId,
|
|
21
|
+
resolveZaloAccount,
|
|
22
|
+
type ResolvedZaloAccount,
|
|
23
|
+
} from "./accounts.js";
|
|
20
24
|
import { zaloMessageActions } from "./actions.js";
|
|
21
25
|
import { ZaloConfigSchema } from "./config-schema.js";
|
|
22
26
|
import { zaloOnboardingAdapter } from "./onboarding.js";
|
|
23
|
-
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
24
27
|
import { probeZalo } from "./probe.js";
|
|
28
|
+
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
25
29
|
import { sendMessageZalo } from "./send.js";
|
|
26
30
|
import { collectZaloStatusIssues } from "./status-issues.js";
|
|
27
31
|
|
|
@@ -39,7 +43,9 @@ const meta = {
|
|
|
39
43
|
|
|
40
44
|
function normalizeZaloMessagingTarget(raw: string): string | undefined {
|
|
41
45
|
const trimmed = raw?.trim();
|
|
42
|
-
if (!trimmed)
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
43
49
|
return trimmed.replace(/^(zalo|zl):/i, "");
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -53,8 +59,8 @@ export const zaloDock: ChannelDock = {
|
|
|
53
59
|
outbound: { textChunkLimit: 2000 },
|
|
54
60
|
config: {
|
|
55
61
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
56
|
-
(resolveZaloAccount({ cfg: cfg
|
|
57
|
-
|
|
62
|
+
(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) =>
|
|
63
|
+
String(entry),
|
|
58
64
|
),
|
|
59
65
|
formatAllowFrom: ({ allowFrom }) =>
|
|
60
66
|
allowFrom
|
|
@@ -87,12 +93,12 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
87
93
|
reload: { configPrefixes: ["channels.zalo"] },
|
|
88
94
|
configSchema: buildChannelConfigSchema(ZaloConfigSchema),
|
|
89
95
|
config: {
|
|
90
|
-
listAccountIds: (cfg) => listZaloAccountIds(cfg
|
|
91
|
-
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg
|
|
92
|
-
defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg
|
|
96
|
+
listAccountIds: (cfg) => listZaloAccountIds(cfg),
|
|
97
|
+
resolveAccount: (cfg, accountId) => resolveZaloAccount({ cfg: cfg, accountId }),
|
|
98
|
+
defaultAccountId: (cfg) => resolveDefaultZaloAccountId(cfg),
|
|
93
99
|
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
94
100
|
setAccountEnabledInConfigSection({
|
|
95
|
-
cfg: cfg
|
|
101
|
+
cfg: cfg,
|
|
96
102
|
sectionKey: "zalo",
|
|
97
103
|
accountId,
|
|
98
104
|
enabled,
|
|
@@ -100,7 +106,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
100
106
|
}),
|
|
101
107
|
deleteAccount: ({ cfg, accountId }) =>
|
|
102
108
|
deleteAccountFromConfigSection({
|
|
103
|
-
cfg: cfg
|
|
109
|
+
cfg: cfg,
|
|
104
110
|
sectionKey: "zalo",
|
|
105
111
|
accountId,
|
|
106
112
|
clearBaseFields: ["botToken", "tokenFile", "name"],
|
|
@@ -114,8 +120,8 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
114
120
|
tokenSource: account.tokenSource,
|
|
115
121
|
}),
|
|
116
122
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
117
|
-
(resolveZaloAccount({ cfg: cfg
|
|
118
|
-
|
|
123
|
+
(resolveZaloAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) =>
|
|
124
|
+
String(entry),
|
|
119
125
|
),
|
|
120
126
|
formatAllowFrom: ({ allowFrom }) =>
|
|
121
127
|
allowFrom
|
|
@@ -127,9 +133,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
127
133
|
security: {
|
|
128
134
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
129
135
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
130
|
-
const useAccountPath = Boolean(
|
|
131
|
-
(cfg as OpenClawConfig).channels?.zalo?.accounts?.[resolvedAccountId],
|
|
132
|
-
);
|
|
136
|
+
const useAccountPath = Boolean(cfg.channels?.zalo?.accounts?.[resolvedAccountId]);
|
|
133
137
|
const basePath = useAccountPath
|
|
134
138
|
? `channels.zalo.accounts.${resolvedAccountId}.`
|
|
135
139
|
: "channels.zalo.";
|
|
@@ -155,7 +159,9 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
155
159
|
targetResolver: {
|
|
156
160
|
looksLikeId: (raw) => {
|
|
157
161
|
const trimmed = raw.trim();
|
|
158
|
-
if (!trimmed)
|
|
162
|
+
if (!trimmed) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
159
165
|
return /^\d{3,}$/.test(trimmed);
|
|
160
166
|
},
|
|
161
167
|
hint: "<chatId>",
|
|
@@ -164,7 +170,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
164
170
|
directory: {
|
|
165
171
|
self: async () => null,
|
|
166
172
|
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
167
|
-
const account = resolveZaloAccount({ cfg: cfg
|
|
173
|
+
const account = resolveZaloAccount({ cfg: cfg, accountId });
|
|
168
174
|
const q = query?.trim().toLowerCase() || "";
|
|
169
175
|
const peers = Array.from(
|
|
170
176
|
new Set(
|
|
@@ -185,7 +191,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
185
191
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
186
192
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
187
193
|
applyAccountNameToChannelSection({
|
|
188
|
-
cfg: cfg
|
|
194
|
+
cfg: cfg,
|
|
189
195
|
channelKey: "zalo",
|
|
190
196
|
accountId,
|
|
191
197
|
name,
|
|
@@ -201,7 +207,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
201
207
|
},
|
|
202
208
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
203
209
|
const namedConfig = applyAccountNameToChannelSection({
|
|
204
|
-
cfg: cfg
|
|
210
|
+
cfg: cfg,
|
|
205
211
|
channelKey: "zalo",
|
|
206
212
|
accountId,
|
|
207
213
|
name: input.name,
|
|
@@ -240,9 +246,9 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
240
246
|
...next.channels?.zalo,
|
|
241
247
|
enabled: true,
|
|
242
248
|
accounts: {
|
|
243
|
-
...
|
|
249
|
+
...next.channels?.zalo?.accounts,
|
|
244
250
|
[accountId]: {
|
|
245
|
-
...
|
|
251
|
+
...next.channels?.zalo?.accounts?.[accountId],
|
|
246
252
|
enabled: true,
|
|
247
253
|
...(input.tokenFile
|
|
248
254
|
? { tokenFile: input.tokenFile }
|
|
@@ -260,16 +266,22 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
260
266
|
idLabel: "zaloUserId",
|
|
261
267
|
normalizeAllowEntry: (entry) => entry.replace(/^(zalo|zl):/i, ""),
|
|
262
268
|
notifyApproval: async ({ cfg, id }) => {
|
|
263
|
-
const account = resolveZaloAccount({ cfg: cfg
|
|
264
|
-
if (!account.token)
|
|
269
|
+
const account = resolveZaloAccount({ cfg: cfg });
|
|
270
|
+
if (!account.token) {
|
|
271
|
+
throw new Error("Zalo token not configured");
|
|
272
|
+
}
|
|
265
273
|
await sendMessageZalo(id, PAIRING_APPROVED_MESSAGE, { token: account.token });
|
|
266
274
|
},
|
|
267
275
|
},
|
|
268
276
|
outbound: {
|
|
269
277
|
deliveryMode: "direct",
|
|
270
278
|
chunker: (text, limit) => {
|
|
271
|
-
if (!text)
|
|
272
|
-
|
|
279
|
+
if (!text) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
if (limit <= 0 || text.length <= limit) {
|
|
283
|
+
return [text];
|
|
284
|
+
}
|
|
273
285
|
const chunks: string[] = [];
|
|
274
286
|
let remaining = text;
|
|
275
287
|
while (remaining.length > limit) {
|
|
@@ -277,15 +289,21 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
277
289
|
const lastNewline = window.lastIndexOf("\n");
|
|
278
290
|
const lastSpace = window.lastIndexOf(" ");
|
|
279
291
|
let breakIdx = lastNewline > 0 ? lastNewline : lastSpace;
|
|
280
|
-
if (breakIdx <= 0)
|
|
292
|
+
if (breakIdx <= 0) {
|
|
293
|
+
breakIdx = limit;
|
|
294
|
+
}
|
|
281
295
|
const rawChunk = remaining.slice(0, breakIdx);
|
|
282
296
|
const chunk = rawChunk.trimEnd();
|
|
283
|
-
if (chunk.length > 0)
|
|
297
|
+
if (chunk.length > 0) {
|
|
298
|
+
chunks.push(chunk);
|
|
299
|
+
}
|
|
284
300
|
const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
|
285
301
|
const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0));
|
|
286
302
|
remaining = remaining.slice(nextStart).trimStart();
|
|
287
303
|
}
|
|
288
|
-
if (remaining.length)
|
|
304
|
+
if (remaining.length) {
|
|
305
|
+
chunks.push(remaining);
|
|
306
|
+
}
|
|
289
307
|
return chunks;
|
|
290
308
|
},
|
|
291
309
|
chunkerMode: "text",
|
|
@@ -293,7 +311,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
293
311
|
sendText: async ({ to, text, accountId, cfg }) => {
|
|
294
312
|
const result = await sendMessageZalo(to, text, {
|
|
295
313
|
accountId: accountId ?? undefined,
|
|
296
|
-
cfg: cfg
|
|
314
|
+
cfg: cfg,
|
|
297
315
|
});
|
|
298
316
|
return {
|
|
299
317
|
channel: "zalo",
|
|
@@ -306,7 +324,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
306
324
|
const result = await sendMessageZalo(to, text, {
|
|
307
325
|
accountId: accountId ?? undefined,
|
|
308
326
|
mediaUrl,
|
|
309
|
-
cfg: cfg
|
|
327
|
+
cfg: cfg,
|
|
310
328
|
});
|
|
311
329
|
return {
|
|
312
330
|
channel: "zalo",
|
|
@@ -366,7 +384,9 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
366
384
|
try {
|
|
367
385
|
const probe = await probeZalo(token, 2500, fetcher);
|
|
368
386
|
const name = probe.ok ? probe.bot?.name?.trim() : null;
|
|
369
|
-
if (name)
|
|
387
|
+
if (name) {
|
|
388
|
+
zaloBotLabel = ` (${name})`;
|
|
389
|
+
}
|
|
370
390
|
ctx.setStatus({
|
|
371
391
|
accountId: account.accountId,
|
|
372
392
|
bot: probe.bot,
|
|
@@ -379,7 +399,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
379
399
|
return monitorZaloProvider({
|
|
380
400
|
token,
|
|
381
401
|
account,
|
|
382
|
-
config: ctx.cfg
|
|
402
|
+
config: ctx.cfg,
|
|
383
403
|
runtime: ctx.runtime,
|
|
384
404
|
abortSignal: ctx.abortSignal,
|
|
385
405
|
useWebhook: Boolean(account.config.webhookUrl),
|
package/src/monitor.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
|
|
3
2
|
import type { OpenClawConfig, MarkdownTableMode } from "openclaw/plugin-sdk";
|
|
4
|
-
|
|
5
3
|
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
6
4
|
import {
|
|
7
5
|
ZaloApiError,
|
|
@@ -52,7 +50,9 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str
|
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
function isSenderAllowed(senderId: string, allowFrom: string[]): boolean {
|
|
55
|
-
if (allowFrom.includes("*"))
|
|
53
|
+
if (allowFrom.includes("*")) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
56
|
const normalizedSenderId = senderId.toLowerCase();
|
|
57
57
|
return allowFrom.some((entry) => {
|
|
58
58
|
const normalized = entry.toLowerCase().replace(/^(zalo|zl):/i, "");
|
|
@@ -108,7 +108,9 @@ const webhookTargets = new Map<string, WebhookTarget[]>();
|
|
|
108
108
|
|
|
109
109
|
function normalizeWebhookPath(raw: string): string {
|
|
110
110
|
const trimmed = raw.trim();
|
|
111
|
-
if (!trimmed)
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
return "/";
|
|
113
|
+
}
|
|
112
114
|
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
113
115
|
if (withSlash.length > 1 && withSlash.endsWith("/")) {
|
|
114
116
|
return withSlash.slice(0, -1);
|
|
@@ -118,7 +120,9 @@ function normalizeWebhookPath(raw: string): string {
|
|
|
118
120
|
|
|
119
121
|
function resolveWebhookPath(webhookPath?: string, webhookUrl?: string): string | null {
|
|
120
122
|
const trimmedPath = webhookPath?.trim();
|
|
121
|
-
if (trimmedPath)
|
|
123
|
+
if (trimmedPath) {
|
|
124
|
+
return normalizeWebhookPath(trimmedPath);
|
|
125
|
+
}
|
|
122
126
|
if (webhookUrl?.trim()) {
|
|
123
127
|
try {
|
|
124
128
|
const parsed = new URL(webhookUrl);
|
|
@@ -137,9 +141,7 @@ export function registerZaloWebhookTarget(target: WebhookTarget): () => void {
|
|
|
137
141
|
const next = [...existing, normalizedTarget];
|
|
138
142
|
webhookTargets.set(key, next);
|
|
139
143
|
return () => {
|
|
140
|
-
const updated = (webhookTargets.get(key) ?? []).filter(
|
|
141
|
-
(entry) => entry !== normalizedTarget,
|
|
142
|
-
);
|
|
144
|
+
const updated = (webhookTargets.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
|
|
143
145
|
if (updated.length > 0) {
|
|
144
146
|
webhookTargets.set(key, updated);
|
|
145
147
|
} else {
|
|
@@ -155,7 +157,9 @@ export async function handleZaloWebhookRequest(
|
|
|
155
157
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
156
158
|
const path = normalizeWebhookPath(url.pathname);
|
|
157
159
|
const targets = webhookTargets.get(path);
|
|
158
|
-
if (!targets || targets.length === 0)
|
|
160
|
+
if (!targets || targets.length === 0) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
159
163
|
|
|
160
164
|
if (req.method !== "POST") {
|
|
161
165
|
res.statusCode = 405;
|
|
@@ -181,12 +185,11 @@ export async function handleZaloWebhookRequest(
|
|
|
181
185
|
|
|
182
186
|
// Zalo sends updates directly as { event_name, message, ... }, not wrapped in { ok, result }
|
|
183
187
|
const raw = body.value;
|
|
184
|
-
const record =
|
|
185
|
-
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : null;
|
|
188
|
+
const record = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : null;
|
|
186
189
|
const update: ZaloUpdate | undefined =
|
|
187
190
|
record && record.ok === true && record.result
|
|
188
191
|
? (record.result as ZaloUpdate)
|
|
189
|
-
: (record as ZaloUpdate | null) ?? undefined;
|
|
192
|
+
: ((record as ZaloUpdate | null) ?? undefined);
|
|
190
193
|
|
|
191
194
|
if (!update?.event_name) {
|
|
192
195
|
res.statusCode = 400;
|
|
@@ -241,7 +244,9 @@ function startPollingLoop(params: {
|
|
|
241
244
|
const pollTimeout = 30;
|
|
242
245
|
|
|
243
246
|
const poll = async () => {
|
|
244
|
-
if (isStopped() || abortSignal.aborted)
|
|
247
|
+
if (isStopped() || abortSignal.aborted) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
245
250
|
|
|
246
251
|
try {
|
|
247
252
|
const response = await getUpdates(token, { timeout: pollTimeout }, fetcher);
|
|
@@ -288,20 +293,13 @@ async function processUpdate(
|
|
|
288
293
|
fetcher?: ZaloFetch,
|
|
289
294
|
): Promise<void> {
|
|
290
295
|
const { event_name, message } = update;
|
|
291
|
-
if (!message)
|
|
296
|
+
if (!message) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
292
299
|
|
|
293
300
|
switch (event_name) {
|
|
294
301
|
case "message.text.received":
|
|
295
|
-
await handleTextMessage(
|
|
296
|
-
message,
|
|
297
|
-
token,
|
|
298
|
-
account,
|
|
299
|
-
config,
|
|
300
|
-
runtime,
|
|
301
|
-
core,
|
|
302
|
-
statusSink,
|
|
303
|
-
fetcher,
|
|
304
|
-
);
|
|
302
|
+
await handleTextMessage(message, token, account, config, runtime, core, statusSink, fetcher);
|
|
305
303
|
break;
|
|
306
304
|
case "message.image.received":
|
|
307
305
|
await handleImageMessage(
|
|
@@ -338,7 +336,9 @@ async function handleTextMessage(
|
|
|
338
336
|
fetcher?: ZaloFetch,
|
|
339
337
|
): Promise<void> {
|
|
340
338
|
const { text } = message;
|
|
341
|
-
if (!text?.trim())
|
|
339
|
+
if (!text?.trim()) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
342
|
|
|
343
343
|
await processMessageWithPipeline({
|
|
344
344
|
message,
|
|
@@ -439,10 +439,7 @@ async function processMessageWithPipeline(params: {
|
|
|
439
439
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
440
440
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
441
441
|
const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
|
|
442
|
-
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(
|
|
443
|
-
rawBody,
|
|
444
|
-
config,
|
|
445
|
-
);
|
|
442
|
+
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
|
|
446
443
|
const storeAllowFrom =
|
|
447
444
|
!isGroup && (dmPolicy !== "open" || shouldComputeAuth)
|
|
448
445
|
? await core.channel.pairing.readAllowFromStore("zalo").catch(() => [])
|
|
@@ -453,7 +450,9 @@ async function processMessageWithPipeline(params: {
|
|
|
453
450
|
const commandAuthorized = shouldComputeAuth
|
|
454
451
|
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
|
455
452
|
useAccessGroups,
|
|
456
|
-
authorizers: [
|
|
453
|
+
authorizers: [
|
|
454
|
+
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
455
|
+
],
|
|
457
456
|
})
|
|
458
457
|
: undefined;
|
|
459
458
|
|
|
@@ -649,11 +648,7 @@ async function deliverZaloReply(params: {
|
|
|
649
648
|
|
|
650
649
|
if (text) {
|
|
651
650
|
const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
|
|
652
|
-
const chunks = core.channel.text.chunkMarkdownTextWithMode(
|
|
653
|
-
text,
|
|
654
|
-
ZALO_TEXT_LIMIT,
|
|
655
|
-
chunkMode,
|
|
656
|
-
);
|
|
651
|
+
const chunks = core.channel.text.chunkMarkdownTextWithMode(text, ZALO_TEXT_LIMIT, chunkMode);
|
|
657
652
|
for (const chunk of chunks) {
|
|
658
653
|
try {
|
|
659
654
|
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
|
@@ -665,9 +660,7 @@ async function deliverZaloReply(params: {
|
|
|
665
660
|
}
|
|
666
661
|
}
|
|
667
662
|
|
|
668
|
-
export async function monitorZaloProvider(
|
|
669
|
-
options: ZaloMonitorOptions,
|
|
670
|
-
): Promise<ZaloMonitorResult> {
|
|
663
|
+
export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<ZaloMonitorResult> {
|
|
671
664
|
const {
|
|
672
665
|
token,
|
|
673
666
|
account,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
1
|
import type { AddressInfo } from "node:net";
|
|
3
|
-
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
|
|
6
2
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
7
5
|
import type { ResolvedZaloAccount } from "./types.js";
|
|
8
6
|
import { handleZaloWebhookRequest, registerZaloWebhookTarget } from "./monitor.js";
|
|
9
7
|
|
|
@@ -16,7 +14,9 @@ async function withServer(
|
|
|
16
14
|
server.listen(0, "127.0.0.1", () => resolve());
|
|
17
15
|
});
|
|
18
16
|
const address = server.address() as AddressInfo | null;
|
|
19
|
-
if (!address)
|
|
17
|
+
if (!address) {
|
|
18
|
+
throw new Error("missing server address");
|
|
19
|
+
}
|
|
20
20
|
try {
|
|
21
21
|
await fn(`http://127.0.0.1:${address.port}`);
|
|
22
22
|
} finally {
|
|
@@ -46,23 +46,26 @@ describe("handleZaloWebhookRequest", () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
|
-
await withServer(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
await withServer(
|
|
50
|
+
async (req, res) => {
|
|
51
|
+
const handled = await handleZaloWebhookRequest(req, res);
|
|
52
|
+
if (!handled) {
|
|
53
|
+
res.statusCode = 404;
|
|
54
|
+
res.end("not found");
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async (baseUrl) => {
|
|
58
|
+
const response = await fetch(`${baseUrl}/hook`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"x-bot-api-secret-token": "secret",
|
|
62
|
+
},
|
|
63
|
+
body: "null",
|
|
64
|
+
});
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
expect(response.status).toBe(400);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
66
69
|
} finally {
|
|
67
70
|
unregister();
|
|
68
71
|
}
|
package/src/onboarding.ts
CHANGED
|
@@ -10,12 +10,7 @@ import {
|
|
|
10
10
|
normalizeAccountId,
|
|
11
11
|
promptAccountId,
|
|
12
12
|
} from "openclaw/plugin-sdk";
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
listZaloAccountIds,
|
|
16
|
-
resolveDefaultZaloAccountId,
|
|
17
|
-
resolveZaloAccount,
|
|
18
|
-
} from "./accounts.js";
|
|
13
|
+
import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
|
|
19
14
|
|
|
20
15
|
const channel = "zalo" as const;
|
|
21
16
|
|
|
@@ -25,7 +20,8 @@ function setZaloDmPolicy(
|
|
|
25
20
|
cfg: OpenClawConfig,
|
|
26
21
|
dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
|
|
27
22
|
) {
|
|
28
|
-
const allowFrom =
|
|
23
|
+
const allowFrom =
|
|
24
|
+
dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.zalo?.allowFrom) : undefined;
|
|
29
25
|
return {
|
|
30
26
|
...cfg,
|
|
31
27
|
channels: {
|
|
@@ -64,17 +60,9 @@ function setZaloUpdateMode(
|
|
|
64
60
|
},
|
|
65
61
|
} as OpenClawConfig;
|
|
66
62
|
}
|
|
67
|
-
const accounts = { ...
|
|
68
|
-
string,
|
|
69
|
-
Record<string, unknown>
|
|
70
|
-
>;
|
|
63
|
+
const accounts = { ...cfg.channels?.zalo?.accounts } as Record<string, Record<string, unknown>>;
|
|
71
64
|
const existing = accounts[accountId] ?? {};
|
|
72
|
-
const {
|
|
73
|
-
webhookUrl: _url,
|
|
74
|
-
webhookSecret: _secret,
|
|
75
|
-
webhookPath: _path,
|
|
76
|
-
...rest
|
|
77
|
-
} = existing;
|
|
65
|
+
const { webhookUrl: _url, webhookSecret: _secret, webhookPath: _path, ...rest } = existing;
|
|
78
66
|
accounts[accountId] = rest;
|
|
79
67
|
return {
|
|
80
68
|
...cfg,
|
|
@@ -103,12 +91,9 @@ function setZaloUpdateMode(
|
|
|
103
91
|
} as OpenClawConfig;
|
|
104
92
|
}
|
|
105
93
|
|
|
106
|
-
const accounts = { ...
|
|
107
|
-
string,
|
|
108
|
-
Record<string, unknown>
|
|
109
|
-
>;
|
|
94
|
+
const accounts = { ...cfg.channels?.zalo?.accounts } as Record<string, Record<string, unknown>>;
|
|
110
95
|
accounts[accountId] = {
|
|
111
|
-
...
|
|
96
|
+
...accounts[accountId],
|
|
112
97
|
webhookUrl,
|
|
113
98
|
webhookSecret,
|
|
114
99
|
webhookPath,
|
|
@@ -152,8 +137,12 @@ async function promptZaloAllowFrom(params: {
|
|
|
152
137
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
153
138
|
validate: (value) => {
|
|
154
139
|
const raw = String(value ?? "").trim();
|
|
155
|
-
if (!raw)
|
|
156
|
-
|
|
140
|
+
if (!raw) {
|
|
141
|
+
return "Required";
|
|
142
|
+
}
|
|
143
|
+
if (!/^\d+$/.test(raw)) {
|
|
144
|
+
return "Use a numeric Zalo user id";
|
|
145
|
+
}
|
|
157
146
|
return undefined;
|
|
158
147
|
},
|
|
159
148
|
});
|
|
@@ -187,9 +176,9 @@ async function promptZaloAllowFrom(params: {
|
|
|
187
176
|
...cfg.channels?.zalo,
|
|
188
177
|
enabled: true,
|
|
189
178
|
accounts: {
|
|
190
|
-
...
|
|
179
|
+
...cfg.channels?.zalo?.accounts,
|
|
191
180
|
[accountId]: {
|
|
192
|
-
...
|
|
181
|
+
...cfg.channels?.zalo?.accounts?.[accountId],
|
|
193
182
|
enabled: cfg.channels?.zalo?.accounts?.[accountId]?.enabled ?? true,
|
|
194
183
|
dmPolicy: "allowlist",
|
|
195
184
|
allowFrom: unique,
|
|
@@ -206,14 +195,14 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
206
195
|
policyKey: "channels.zalo.dmPolicy",
|
|
207
196
|
allowFromKey: "channels.zalo.allowFrom",
|
|
208
197
|
getCurrent: (cfg) => (cfg.channels?.zalo?.dmPolicy ?? "pairing") as "pairing",
|
|
209
|
-
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg
|
|
198
|
+
setPolicy: (cfg, policy) => setZaloDmPolicy(cfg, policy),
|
|
210
199
|
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
|
|
211
200
|
const id =
|
|
212
201
|
accountId && normalizeAccountId(accountId)
|
|
213
|
-
? normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID
|
|
214
|
-
: resolveDefaultZaloAccountId(cfg
|
|
202
|
+
? (normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID)
|
|
203
|
+
: resolveDefaultZaloAccountId(cfg);
|
|
215
204
|
return promptZaloAllowFrom({
|
|
216
|
-
cfg: cfg
|
|
205
|
+
cfg: cfg,
|
|
217
206
|
prompter,
|
|
218
207
|
accountId: id,
|
|
219
208
|
});
|
|
@@ -224,8 +213,8 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
224
213
|
channel,
|
|
225
214
|
dmPolicy,
|
|
226
215
|
getStatus: async ({ cfg }) => {
|
|
227
|
-
const configured = listZaloAccountIds(cfg
|
|
228
|
-
Boolean(resolveZaloAccount({ cfg: cfg
|
|
216
|
+
const configured = listZaloAccountIds(cfg).some((accountId) =>
|
|
217
|
+
Boolean(resolveZaloAccount({ cfg: cfg, accountId }).token),
|
|
229
218
|
);
|
|
230
219
|
return {
|
|
231
220
|
channel,
|
|
@@ -235,15 +224,19 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
235
224
|
quickstartScore: configured ? 1 : 10,
|
|
236
225
|
};
|
|
237
226
|
},
|
|
238
|
-
configure: async ({
|
|
227
|
+
configure: async ({
|
|
228
|
+
cfg,
|
|
229
|
+
prompter,
|
|
230
|
+
accountOverrides,
|
|
231
|
+
shouldPromptAccountIds,
|
|
232
|
+
forceAllowFrom,
|
|
233
|
+
}) => {
|
|
239
234
|
const zaloOverride = accountOverrides.zalo?.trim();
|
|
240
|
-
const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg
|
|
241
|
-
let zaloAccountId = zaloOverride
|
|
242
|
-
? normalizeAccountId(zaloOverride)
|
|
243
|
-
: defaultZaloAccountId;
|
|
235
|
+
const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg);
|
|
236
|
+
let zaloAccountId = zaloOverride ? normalizeAccountId(zaloOverride) : defaultZaloAccountId;
|
|
244
237
|
if (shouldPromptAccountIds && !zaloOverride) {
|
|
245
238
|
zaloAccountId = await promptAccountId({
|
|
246
|
-
cfg: cfg
|
|
239
|
+
cfg: cfg,
|
|
247
240
|
prompter,
|
|
248
241
|
label: "Zalo",
|
|
249
242
|
currentId: zaloAccountId,
|
|
@@ -252,7 +245,7 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
252
245
|
});
|
|
253
246
|
}
|
|
254
247
|
|
|
255
|
-
let next = cfg
|
|
248
|
+
let next = cfg;
|
|
256
249
|
const resolvedAccount = resolveZaloAccount({ cfg: next, accountId: zaloAccountId });
|
|
257
250
|
const accountConfigured = Boolean(resolvedAccount.token);
|
|
258
251
|
const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
|
|
@@ -333,9 +326,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
333
326
|
...next.channels?.zalo,
|
|
334
327
|
enabled: true,
|
|
335
328
|
accounts: {
|
|
336
|
-
...
|
|
329
|
+
...next.channels?.zalo?.accounts,
|
|
337
330
|
[zaloAccountId]: {
|
|
338
|
-
...
|
|
331
|
+
...next.channels?.zalo?.accounts?.[zaloAccountId],
|
|
339
332
|
enabled: true,
|
|
340
333
|
botToken: token,
|
|
341
334
|
},
|
|
@@ -354,7 +347,8 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
354
347
|
const webhookUrl = String(
|
|
355
348
|
await prompter.text({
|
|
356
349
|
message: "Webhook URL (https://...) ",
|
|
357
|
-
validate: (value) =>
|
|
350
|
+
validate: (value) =>
|
|
351
|
+
value?.trim()?.startsWith("https://") ? undefined : "HTTPS URL required",
|
|
358
352
|
}),
|
|
359
353
|
).trim();
|
|
360
354
|
const defaultPath = (() => {
|
|
@@ -369,7 +363,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
369
363
|
message: "Webhook secret (8-256 chars)",
|
|
370
364
|
validate: (value) => {
|
|
371
365
|
const raw = String(value ?? "");
|
|
372
|
-
if (raw.length < 8 || raw.length > 256)
|
|
366
|
+
if (raw.length < 8 || raw.length > 256) {
|
|
367
|
+
return "8-256 chars";
|
|
368
|
+
}
|
|
373
369
|
return undefined;
|
|
374
370
|
},
|
|
375
371
|
}),
|
package/src/proxy.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
|
2
1
|
import type { Dispatcher } from "undici";
|
|
3
|
-
|
|
2
|
+
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
|
4
3
|
import type { ZaloFetch } from "./api.js";
|
|
5
4
|
|
|
6
5
|
const proxyCache = new Map<string, ZaloFetch>();
|
|
7
6
|
|
|
8
7
|
export function resolveZaloProxyFetch(proxyUrl?: string | null): ZaloFetch | undefined {
|
|
9
8
|
const trimmed = proxyUrl?.trim();
|
|
10
|
-
if (!trimmed)
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
11
12
|
const cached = proxyCache.get(trimmed);
|
|
12
|
-
if (cached)
|
|
13
|
+
if (cached) {
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
13
16
|
const agent = new ProxyAgent(trimmed);
|
|
14
17
|
const fetcher: ZaloFetch = (input, init) =>
|
|
15
|
-
undiciFetch(input, { ...
|
|
18
|
+
undiciFetch(input, { ...init, dispatcher: agent as Dispatcher });
|
|
16
19
|
proxyCache.set(trimmed, fetcher);
|
|
17
20
|
return fetcher;
|
|
18
21
|
}
|
package/src/send.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
2
|
import type { ZaloFetch } from "./api.js";
|
|
4
|
-
import { sendMessage, sendPhoto } from "./api.js";
|
|
5
3
|
import { resolveZaloAccount } from "./accounts.js";
|
|
4
|
+
import { sendMessage, sendPhoto } from "./api.js";
|
|
6
5
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
7
6
|
import { resolveZaloToken } from "./token.js";
|
|
8
7
|
|
|
@@ -65,10 +64,14 @@ export async function sendMessageZalo(
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
try {
|
|
68
|
-
const response = await sendMessage(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const response = await sendMessage(
|
|
68
|
+
token,
|
|
69
|
+
{
|
|
70
|
+
chat_id: chatId.trim(),
|
|
71
|
+
text: text.slice(0, 2000),
|
|
72
|
+
},
|
|
73
|
+
fetcher,
|
|
74
|
+
);
|
|
72
75
|
|
|
73
76
|
if (response.ok && response.result) {
|
|
74
77
|
return { ok: true, messageId: response.result.message_id };
|
|
@@ -100,11 +103,15 @@ export async function sendPhotoZalo(
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
try {
|
|
103
|
-
const response = await sendPhoto(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const response = await sendPhoto(
|
|
107
|
+
token,
|
|
108
|
+
{
|
|
109
|
+
chat_id: chatId.trim(),
|
|
110
|
+
photo: photoUrl.trim(),
|
|
111
|
+
caption: options.caption?.slice(0, 2000),
|
|
112
|
+
},
|
|
113
|
+
fetcher,
|
|
114
|
+
);
|
|
108
115
|
|
|
109
116
|
if (response.ok && response.result) {
|
|
110
117
|
return { ok: true, messageId: response.result.message_id };
|
package/src/status-issues.ts
CHANGED
|
@@ -14,7 +14,9 @@ const asString = (value: unknown): string | undefined =>
|
|
|
14
14
|
typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined;
|
|
15
15
|
|
|
16
16
|
function readZaloAccountStatus(value: ChannelAccountSnapshot): ZaloAccountStatus | null {
|
|
17
|
-
if (!isRecord(value))
|
|
17
|
+
if (!isRecord(value)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
18
20
|
return {
|
|
19
21
|
accountId: value.accountId,
|
|
20
22
|
enabled: value.enabled,
|
|
@@ -23,25 +25,26 @@ function readZaloAccountStatus(value: ChannelAccountSnapshot): ZaloAccountStatus
|
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export function collectZaloStatusIssues(
|
|
27
|
-
accounts: ChannelAccountSnapshot[],
|
|
28
|
-
): ChannelStatusIssue[] {
|
|
28
|
+
export function collectZaloStatusIssues(accounts: ChannelAccountSnapshot[]): ChannelStatusIssue[] {
|
|
29
29
|
const issues: ChannelStatusIssue[] = [];
|
|
30
30
|
for (const entry of accounts) {
|
|
31
31
|
const account = readZaloAccountStatus(entry);
|
|
32
|
-
if (!account)
|
|
32
|
+
if (!account) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
33
35
|
const accountId = asString(account.accountId) ?? "default";
|
|
34
36
|
const enabled = account.enabled !== false;
|
|
35
37
|
const configured = account.configured === true;
|
|
36
|
-
if (!enabled || !configured)
|
|
38
|
+
if (!enabled || !configured) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
37
41
|
|
|
38
42
|
if (account.dmPolicy === "open") {
|
|
39
43
|
issues.push({
|
|
40
44
|
channel: "zalo",
|
|
41
45
|
accountId,
|
|
42
46
|
kind: "config",
|
|
43
|
-
message:
|
|
44
|
-
'Zalo dmPolicy is "open", allowing any user to message the bot without pairing.',
|
|
47
|
+
message: 'Zalo dmPolicy is "open", allowing any user to message the bot without pairing.',
|
|
45
48
|
fix: 'Set channels.zalo.dmPolicy to "pairing" or "allowlist" to restrict access.',
|
|
46
49
|
});
|
|
47
50
|
}
|
package/src/token.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
2
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
4
|
-
|
|
5
3
|
import type { ZaloConfig } from "./types.js";
|
|
6
4
|
|
|
7
5
|
export type ZaloTokenResolution = {
|
|
@@ -23,12 +21,16 @@ export function resolveZaloToken(
|
|
|
23
21
|
|
|
24
22
|
if (accountConfig) {
|
|
25
23
|
const token = accountConfig.botToken?.trim();
|
|
26
|
-
if (token)
|
|
24
|
+
if (token) {
|
|
25
|
+
return { token, source: "config" };
|
|
26
|
+
}
|
|
27
27
|
const tokenFile = accountConfig.tokenFile?.trim();
|
|
28
28
|
if (tokenFile) {
|
|
29
29
|
try {
|
|
30
30
|
const fileToken = readFileSync(tokenFile, "utf8").trim();
|
|
31
|
-
if (fileToken)
|
|
31
|
+
if (fileToken) {
|
|
32
|
+
return { token: fileToken, source: "configFile" };
|
|
33
|
+
}
|
|
32
34
|
} catch {
|
|
33
35
|
// ignore read failures
|
|
34
36
|
}
|
|
@@ -37,18 +39,24 @@ export function resolveZaloToken(
|
|
|
37
39
|
|
|
38
40
|
if (isDefaultAccount) {
|
|
39
41
|
const token = baseConfig?.botToken?.trim();
|
|
40
|
-
if (token)
|
|
42
|
+
if (token) {
|
|
43
|
+
return { token, source: "config" };
|
|
44
|
+
}
|
|
41
45
|
const tokenFile = baseConfig?.tokenFile?.trim();
|
|
42
46
|
if (tokenFile) {
|
|
43
47
|
try {
|
|
44
48
|
const fileToken = readFileSync(tokenFile, "utf8").trim();
|
|
45
|
-
if (fileToken)
|
|
49
|
+
if (fileToken) {
|
|
50
|
+
return { token: fileToken, source: "configFile" };
|
|
51
|
+
}
|
|
46
52
|
} catch {
|
|
47
53
|
// ignore read failures
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
const envToken = process.env.ZALO_BOT_TOKEN?.trim();
|
|
51
|
-
if (envToken)
|
|
57
|
+
if (envToken) {
|
|
58
|
+
return { token: envToken, source: "env" };
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
return { token: "", source: "none" };
|