@openclaw/zalo 2026.3.2 → 2026.3.8-beta.1
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 +24 -0
- package/index.ts +2 -2
- package/package.json +3 -2
- package/src/accounts.ts +5 -37
- package/src/actions.ts +2 -2
- package/src/api.test.ts +63 -0
- package/src/api.ts +36 -6
- package/src/channel.directory.test.ts +1 -1
- package/src/channel.sendpayload.test.ts +1 -1
- package/src/channel.startup.test.ts +100 -0
- package/src/channel.ts +107 -163
- package/src/config-schema.ts +8 -9
- package/src/group-access.ts +2 -2
- package/src/monitor.lifecycle.test.ts +213 -0
- package/src/monitor.ts +185 -71
- package/src/monitor.webhook.test.ts +40 -32
- package/src/monitor.webhook.ts +77 -92
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.ts +38 -39
- package/src/probe.ts +1 -1
- package/src/runtime.ts +5 -13
- package/src/secret-input.ts +8 -14
- package/src/send.ts +29 -24
- package/src/status-issues.ts +1 -1
- package/src/token.ts +24 -30
- package/src/types.ts +1 -1
package/src/onboarding.ts
CHANGED
|
@@ -4,16 +4,17 @@ import type {
|
|
|
4
4
|
OpenClawConfig,
|
|
5
5
|
SecretInput,
|
|
6
6
|
WizardPrompter,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
7
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
buildSingleChannelSecretPromptState,
|
|
10
10
|
DEFAULT_ACCOUNT_ID,
|
|
11
11
|
hasConfiguredSecretInput,
|
|
12
12
|
mergeAllowFromEntries,
|
|
13
13
|
normalizeAccountId,
|
|
14
|
-
promptAccountId,
|
|
15
14
|
promptSingleChannelSecretInput,
|
|
16
|
-
|
|
15
|
+
resolveAccountIdForConfigure,
|
|
16
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
17
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
17
18
|
import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js";
|
|
18
19
|
|
|
19
20
|
const channel = "zalo" as const;
|
|
@@ -24,19 +25,11 @@ function setZaloDmPolicy(
|
|
|
24
25
|
cfg: OpenClawConfig,
|
|
25
26
|
dmPolicy: "pairing" | "allowlist" | "open" | "disabled",
|
|
26
27
|
) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...cfg.channels,
|
|
33
|
-
zalo: {
|
|
34
|
-
...cfg.channels?.zalo,
|
|
35
|
-
dmPolicy,
|
|
36
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
} as OpenClawConfig;
|
|
28
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
29
|
+
cfg,
|
|
30
|
+
channel: "zalo",
|
|
31
|
+
dmPolicy,
|
|
32
|
+
}) as OpenClawConfig;
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
function setZaloUpdateMode(
|
|
@@ -240,19 +233,16 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
240
233
|
shouldPromptAccountIds,
|
|
241
234
|
forceAllowFrom,
|
|
242
235
|
}) => {
|
|
243
|
-
const zaloOverride = accountOverrides.zalo?.trim();
|
|
244
236
|
const defaultZaloAccountId = resolveDefaultZaloAccountId(cfg);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
255
|
-
}
|
|
237
|
+
const zaloAccountId = await resolveAccountIdForConfigure({
|
|
238
|
+
cfg,
|
|
239
|
+
prompter,
|
|
240
|
+
label: "Zalo",
|
|
241
|
+
accountOverride: accountOverrides.zalo,
|
|
242
|
+
shouldPromptAccountIds,
|
|
243
|
+
listAccountIds: listZaloAccountIds,
|
|
244
|
+
defaultAccountId: defaultZaloAccountId,
|
|
245
|
+
});
|
|
256
246
|
|
|
257
247
|
let next = cfg;
|
|
258
248
|
const resolvedAccount = resolveZaloAccount({
|
|
@@ -262,10 +252,15 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
262
252
|
});
|
|
263
253
|
const accountConfigured = Boolean(resolvedAccount.token);
|
|
264
254
|
const allowEnv = zaloAccountId === DEFAULT_ACCOUNT_ID;
|
|
265
|
-
const canUseEnv = allowEnv && Boolean(process.env.ZALO_BOT_TOKEN?.trim());
|
|
266
255
|
const hasConfigToken = Boolean(
|
|
267
256
|
hasConfiguredSecretInput(resolvedAccount.config.botToken) || resolvedAccount.config.tokenFile,
|
|
268
257
|
);
|
|
258
|
+
const tokenPromptState = buildSingleChannelSecretPromptState({
|
|
259
|
+
accountConfigured,
|
|
260
|
+
hasConfigToken,
|
|
261
|
+
allowEnv,
|
|
262
|
+
envValue: process.env.ZALO_BOT_TOKEN,
|
|
263
|
+
});
|
|
269
264
|
|
|
270
265
|
let token: SecretInput | null = null;
|
|
271
266
|
if (!accountConfigured) {
|
|
@@ -276,9 +271,9 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
276
271
|
prompter,
|
|
277
272
|
providerHint: "zalo",
|
|
278
273
|
credentialLabel: "bot token",
|
|
279
|
-
accountConfigured,
|
|
280
|
-
canUseEnv: canUseEnv
|
|
281
|
-
hasConfigToken,
|
|
274
|
+
accountConfigured: tokenPromptState.accountConfigured,
|
|
275
|
+
canUseEnv: tokenPromptState.canUseEnv,
|
|
276
|
+
hasConfigToken: tokenPromptState.hasConfigToken,
|
|
282
277
|
envPrompt: "ZALO_BOT_TOKEN detected. Use env var?",
|
|
283
278
|
keepPrompt: "Zalo token already configured. Keep it?",
|
|
284
279
|
inputPrompt: "Enter Zalo bot token",
|
|
@@ -360,9 +355,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
360
355
|
prompter,
|
|
361
356
|
providerHint: "zalo-webhook",
|
|
362
357
|
credentialLabel: "webhook secret",
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
358
|
+
...buildSingleChannelSecretPromptState({
|
|
359
|
+
accountConfigured: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
|
|
360
|
+
hasConfigToken: hasConfiguredSecretInput(resolvedAccount.config.webhookSecret),
|
|
361
|
+
allowEnv: false,
|
|
362
|
+
}),
|
|
366
363
|
envPrompt: "",
|
|
367
364
|
keepPrompt: "Zalo webhook secret already configured. Keep it?",
|
|
368
365
|
inputPrompt: "Webhook secret (8-256 chars)",
|
|
@@ -379,9 +376,11 @@ export const zaloOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
379
376
|
prompter,
|
|
380
377
|
providerHint: "zalo-webhook",
|
|
381
378
|
credentialLabel: "webhook secret",
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
379
|
+
...buildSingleChannelSecretPromptState({
|
|
380
|
+
accountConfigured: false,
|
|
381
|
+
hasConfigToken: false,
|
|
382
|
+
allowEnv: false,
|
|
383
|
+
}),
|
|
385
384
|
envPrompt: "",
|
|
386
385
|
keepPrompt: "Zalo webhook secret already configured. Keep it?",
|
|
387
386
|
inputPrompt: "Webhook secret (8-256 chars)",
|
package/src/probe.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaseProbeResult } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { BaseProbeResult } from "openclaw/plugin-sdk/zalo";
|
|
2
2
|
import { getMe, ZaloApiError, type ZaloBotInfo, type ZaloFetch } from "./api.js";
|
|
3
3
|
|
|
4
4
|
export type ZaloProbeResult = BaseProbeResult<string> & {
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
2
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk/zalo";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
runtime = next;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getZaloRuntime(): PluginRuntime {
|
|
10
|
-
if (!runtime) {
|
|
11
|
-
throw new Error("Zalo runtime not initialized");
|
|
12
|
-
}
|
|
13
|
-
return runtime;
|
|
14
|
-
}
|
|
4
|
+
const { setRuntime: setZaloRuntime, getRuntime: getZaloRuntime } =
|
|
5
|
+
createPluginRuntimeStore<PluginRuntime>("Zalo runtime not initialized");
|
|
6
|
+
export { getZaloRuntime, setZaloRuntime };
|
package/src/secret-input.ts
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildSecretInputSchema,
|
|
2
3
|
hasConfiguredSecretInput,
|
|
3
4
|
normalizeResolvedSecretInputString,
|
|
4
5
|
normalizeSecretInputString,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import { z } from "zod";
|
|
6
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
7
7
|
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
source: z.enum(["env", "file", "exec"]),
|
|
15
|
-
provider: z.string().min(1),
|
|
16
|
-
id: z.string().min(1),
|
|
17
|
-
}),
|
|
18
|
-
]);
|
|
19
|
-
}
|
|
8
|
+
export {
|
|
9
|
+
buildSecretInputSchema,
|
|
10
|
+
hasConfiguredSecretInput,
|
|
11
|
+
normalizeResolvedSecretInputString,
|
|
12
|
+
normalizeSecretInputString,
|
|
13
|
+
};
|
package/src/send.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo";
|
|
2
2
|
import { resolveZaloAccount } from "./accounts.js";
|
|
3
3
|
import type { ZaloFetch } from "./api.js";
|
|
4
4
|
import { sendMessage, sendPhoto } from "./api.js";
|
|
@@ -40,37 +40,47 @@ function resolveSendContext(options: ZaloSendOptions): {
|
|
|
40
40
|
return { token, fetcher: resolveZaloProxyFetch(proxy) };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
function resolveValidatedSendContext(
|
|
44
44
|
chatId: string,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
): Promise<ZaloSendResult> {
|
|
45
|
+
options: ZaloSendOptions,
|
|
46
|
+
): { ok: true; chatId: string; token: string; fetcher?: ZaloFetch } | { ok: false; error: string } {
|
|
48
47
|
const { token, fetcher } = resolveSendContext(options);
|
|
49
|
-
|
|
50
48
|
if (!token) {
|
|
51
49
|
return { ok: false, error: "No Zalo bot token configured" };
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
if (!
|
|
51
|
+
const trimmedChatId = chatId?.trim();
|
|
52
|
+
if (!trimmedChatId) {
|
|
55
53
|
return { ok: false, error: "No chat_id provided" };
|
|
56
54
|
}
|
|
55
|
+
return { ok: true, chatId: trimmedChatId, token, fetcher };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function sendMessageZalo(
|
|
59
|
+
chatId: string,
|
|
60
|
+
text: string,
|
|
61
|
+
options: ZaloSendOptions = {},
|
|
62
|
+
): Promise<ZaloSendResult> {
|
|
63
|
+
const context = resolveValidatedSendContext(chatId, options);
|
|
64
|
+
if (!context.ok) {
|
|
65
|
+
return { ok: false, error: context.error };
|
|
66
|
+
}
|
|
57
67
|
|
|
58
68
|
if (options.mediaUrl) {
|
|
59
|
-
return sendPhotoZalo(chatId, options.mediaUrl, {
|
|
69
|
+
return sendPhotoZalo(context.chatId, options.mediaUrl, {
|
|
60
70
|
...options,
|
|
61
|
-
token,
|
|
71
|
+
token: context.token,
|
|
62
72
|
caption: text || options.caption,
|
|
63
73
|
});
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
try {
|
|
67
77
|
const response = await sendMessage(
|
|
68
|
-
token,
|
|
78
|
+
context.token,
|
|
69
79
|
{
|
|
70
|
-
chat_id: chatId
|
|
80
|
+
chat_id: context.chatId,
|
|
71
81
|
text: text.slice(0, 2000),
|
|
72
82
|
},
|
|
73
|
-
fetcher,
|
|
83
|
+
context.fetcher,
|
|
74
84
|
);
|
|
75
85
|
|
|
76
86
|
if (response.ok && response.result) {
|
|
@@ -88,14 +98,9 @@ export async function sendPhotoZalo(
|
|
|
88
98
|
photoUrl: string,
|
|
89
99
|
options: ZaloSendOptions = {},
|
|
90
100
|
): Promise<ZaloSendResult> {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return { ok: false, error: "No Zalo bot token configured" };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!chatId?.trim()) {
|
|
98
|
-
return { ok: false, error: "No chat_id provided" };
|
|
101
|
+
const context = resolveValidatedSendContext(chatId, options);
|
|
102
|
+
if (!context.ok) {
|
|
103
|
+
return { ok: false, error: context.error };
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
if (!photoUrl?.trim()) {
|
|
@@ -104,13 +109,13 @@ export async function sendPhotoZalo(
|
|
|
104
109
|
|
|
105
110
|
try {
|
|
106
111
|
const response = await sendPhoto(
|
|
107
|
-
token,
|
|
112
|
+
context.token,
|
|
108
113
|
{
|
|
109
|
-
chat_id: chatId
|
|
114
|
+
chat_id: context.chatId,
|
|
110
115
|
photo: photoUrl.trim(),
|
|
111
116
|
caption: options.caption?.slice(0, 2000),
|
|
112
117
|
},
|
|
113
|
-
fetcher,
|
|
118
|
+
context.fetcher,
|
|
114
119
|
);
|
|
115
120
|
|
|
116
121
|
if (response.ok && response.result) {
|
package/src/status-issues.ts
CHANGED
package/src/token.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import type { BaseTokenResolution } from "openclaw/plugin-sdk";
|
|
3
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
+
import type { BaseTokenResolution } from "openclaw/plugin-sdk/zalo";
|
|
4
4
|
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
|
5
5
|
import type { ZaloConfig } from "./types.js";
|
|
6
6
|
|
|
@@ -8,6 +8,19 @@ export type ZaloTokenResolution = BaseTokenResolution & {
|
|
|
8
8
|
source: "env" | "config" | "configFile" | "none";
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
function readTokenFromFile(tokenFile: string | undefined): string {
|
|
12
|
+
const trimmedPath = tokenFile?.trim();
|
|
13
|
+
if (!trimmedPath) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(trimmedPath, "utf8").trim();
|
|
18
|
+
} catch {
|
|
19
|
+
// ignore read failures
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
export function resolveZaloToken(
|
|
12
25
|
config: ZaloConfig | undefined,
|
|
13
26
|
accountId?: string | null,
|
|
@@ -44,28 +57,16 @@ export function resolveZaloToken(
|
|
|
44
57
|
if (token) {
|
|
45
58
|
return { token, source: "config" };
|
|
46
59
|
}
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
const fileToken = readFileSync(tokenFile, "utf8").trim();
|
|
51
|
-
if (fileToken) {
|
|
52
|
-
return { token: fileToken, source: "configFile" };
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// ignore read failures
|
|
56
|
-
}
|
|
60
|
+
const fileToken = readTokenFromFile(accountConfig.tokenFile);
|
|
61
|
+
if (fileToken) {
|
|
62
|
+
return { token: fileToken, source: "configFile" };
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (fileToken) {
|
|
65
|
-
return { token: fileToken, source: "configFile" };
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// ignore read failures
|
|
66
|
+
if (!accountHasBotToken) {
|
|
67
|
+
const fileToken = readTokenFromFile(accountConfig?.tokenFile);
|
|
68
|
+
if (fileToken) {
|
|
69
|
+
return { token: fileToken, source: "configFile" };
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -79,16 +80,9 @@ export function resolveZaloToken(
|
|
|
79
80
|
if (token) {
|
|
80
81
|
return { token, source: "config" };
|
|
81
82
|
}
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
const fileToken = readFileSync(tokenFile, "utf8").trim();
|
|
86
|
-
if (fileToken) {
|
|
87
|
-
return { token: fileToken, source: "configFile" };
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
// ignore read failures
|
|
91
|
-
}
|
|
83
|
+
const fileToken = readTokenFromFile(baseConfig?.tokenFile);
|
|
84
|
+
if (fileToken) {
|
|
85
|
+
return { token: fileToken, source: "configFile" };
|
|
92
86
|
}
|
|
93
87
|
}
|
|
94
88
|
|
package/src/types.ts
CHANGED