@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.3.8-beta.1
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.3.8
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.3.7
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
21
|
+
## 2026.3.3
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- Version alignment with core OpenClaw release numbers.
|
|
26
|
+
|
|
3
27
|
## 2026.3.2
|
|
4
28
|
|
|
5
29
|
### Changes
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/zalo";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalo";
|
|
3
3
|
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
|
4
4
|
import { setZaloRuntime } from "./src/runtime.js";
|
|
5
5
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/zalo",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.8-beta.1",
|
|
4
4
|
"description": "OpenClaw Zalo channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"undici": "7.22.0"
|
|
7
|
+
"undici": "7.22.0",
|
|
8
|
+
"zod": "^4.3.6"
|
|
8
9
|
},
|
|
9
10
|
"openclaw": {
|
|
10
11
|
"extensions": [
|
package/src/accounts.ts
CHANGED
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_ACCOUNT_ID,
|
|
4
|
-
normalizeAccountId,
|
|
5
|
-
normalizeOptionalAccountId,
|
|
6
|
-
} from "openclaw/plugin-sdk/account-id";
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/zalo";
|
|
7
3
|
import { resolveZaloToken } from "./token.js";
|
|
8
4
|
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
|
|
9
5
|
|
|
10
6
|
export type { ResolvedZaloAccount };
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
return Object.keys(accounts).filter(Boolean);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function listZaloAccountIds(cfg: OpenClawConfig): string[] {
|
|
21
|
-
const ids = listConfiguredAccountIds(cfg);
|
|
22
|
-
if (ids.length === 0) {
|
|
23
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
24
|
-
}
|
|
25
|
-
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string {
|
|
29
|
-
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
|
30
|
-
const preferred = normalizeOptionalAccountId(zaloConfig?.defaultAccount);
|
|
31
|
-
if (
|
|
32
|
-
preferred &&
|
|
33
|
-
listZaloAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
34
|
-
) {
|
|
35
|
-
return preferred;
|
|
36
|
-
}
|
|
37
|
-
const ids = listZaloAccountIds(cfg);
|
|
38
|
-
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
39
|
-
return DEFAULT_ACCOUNT_ID;
|
|
40
|
-
}
|
|
41
|
-
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
42
|
-
}
|
|
8
|
+
const { listAccountIds: listZaloAccountIds, resolveDefaultAccountId: resolveDefaultZaloAccountId } =
|
|
9
|
+
createAccountListHelpers("zalo");
|
|
10
|
+
export { listZaloAccountIds, resolveDefaultZaloAccountId };
|
|
43
11
|
|
|
44
12
|
function resolveAccountConfig(
|
|
45
13
|
cfg: OpenClawConfig,
|
package/src/actions.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type {
|
|
|
2
2
|
ChannelMessageActionAdapter,
|
|
3
3
|
ChannelMessageActionName,
|
|
4
4
|
OpenClawConfig,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk";
|
|
5
|
+
} from "openclaw/plugin-sdk/zalo";
|
|
6
|
+
import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk/zalo";
|
|
7
7
|
import { listEnabledZaloAccounts } from "./accounts.js";
|
|
8
8
|
import { sendMessageZalo } from "./send.js";
|
|
9
9
|
|
package/src/api.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { deleteWebhook, getWebhookInfo, sendChatAction, type ZaloFetch } from "./api.js";
|
|
3
|
+
|
|
4
|
+
describe("Zalo API request methods", () => {
|
|
5
|
+
it("uses POST for getWebhookInfo", async () => {
|
|
6
|
+
const fetcher = vi.fn<ZaloFetch>(
|
|
7
|
+
async () => new Response(JSON.stringify({ ok: true, result: {} })),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
await getWebhookInfo("test-token", fetcher);
|
|
11
|
+
|
|
12
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
13
|
+
const [, init] = fetcher.mock.calls[0] ?? [];
|
|
14
|
+
expect(init?.method).toBe("POST");
|
|
15
|
+
expect(init?.headers).toEqual({ "Content-Type": "application/json" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("keeps POST for deleteWebhook", async () => {
|
|
19
|
+
const fetcher = vi.fn<ZaloFetch>(
|
|
20
|
+
async () => new Response(JSON.stringify({ ok: true, result: {} })),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
await deleteWebhook("test-token", fetcher);
|
|
24
|
+
|
|
25
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
26
|
+
const [, init] = fetcher.mock.calls[0] ?? [];
|
|
27
|
+
expect(init?.method).toBe("POST");
|
|
28
|
+
expect(init?.headers).toEqual({ "Content-Type": "application/json" });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("aborts sendChatAction when the typing timeout elapses", async () => {
|
|
32
|
+
vi.useFakeTimers();
|
|
33
|
+
try {
|
|
34
|
+
const fetcher = vi.fn<ZaloFetch>(
|
|
35
|
+
(_, init) =>
|
|
36
|
+
new Promise<Response>((_, reject) => {
|
|
37
|
+
init?.signal?.addEventListener("abort", () => reject(new Error("aborted")), {
|
|
38
|
+
once: true,
|
|
39
|
+
});
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const promise = sendChatAction(
|
|
44
|
+
"test-token",
|
|
45
|
+
{
|
|
46
|
+
chat_id: "chat-123",
|
|
47
|
+
action: "typing",
|
|
48
|
+
},
|
|
49
|
+
fetcher,
|
|
50
|
+
25,
|
|
51
|
+
);
|
|
52
|
+
const rejected = expect(promise).rejects.toThrow("aborted");
|
|
53
|
+
|
|
54
|
+
await vi.advanceTimersByTimeAsync(25);
|
|
55
|
+
|
|
56
|
+
await rejected;
|
|
57
|
+
const [, init] = fetcher.mock.calls[0] ?? [];
|
|
58
|
+
expect(init?.signal?.aborted).toBe(true);
|
|
59
|
+
} finally {
|
|
60
|
+
vi.useRealTimers();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/api.ts
CHANGED
|
@@ -58,11 +58,22 @@ export type ZaloSendPhotoParams = {
|
|
|
58
58
|
caption?: string;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
export type ZaloSendChatActionParams = {
|
|
62
|
+
chat_id: string;
|
|
63
|
+
action: "typing" | "upload_photo";
|
|
64
|
+
};
|
|
65
|
+
|
|
61
66
|
export type ZaloSetWebhookParams = {
|
|
62
67
|
url: string;
|
|
63
68
|
secret_token: string;
|
|
64
69
|
};
|
|
65
70
|
|
|
71
|
+
export type ZaloWebhookInfo = {
|
|
72
|
+
url?: string;
|
|
73
|
+
updated_at?: number;
|
|
74
|
+
has_custom_certificate?: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
66
77
|
export type ZaloGetUpdatesParams = {
|
|
67
78
|
/** Timeout in seconds (passed as string to API) */
|
|
68
79
|
timeout?: number;
|
|
@@ -161,6 +172,21 @@ export async function sendPhoto(
|
|
|
161
172
|
return callZaloApi<ZaloMessage>("sendPhoto", token, params, { fetch: fetcher });
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Send a temporary chat action such as typing.
|
|
177
|
+
*/
|
|
178
|
+
export async function sendChatAction(
|
|
179
|
+
token: string,
|
|
180
|
+
params: ZaloSendChatActionParams,
|
|
181
|
+
fetcher?: ZaloFetch,
|
|
182
|
+
timeoutMs?: number,
|
|
183
|
+
): Promise<ZaloApiResponse<boolean>> {
|
|
184
|
+
return callZaloApi<boolean>("sendChatAction", token, params, {
|
|
185
|
+
timeoutMs,
|
|
186
|
+
fetch: fetcher,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
164
190
|
/**
|
|
165
191
|
* Get updates using long polling (dev/testing only)
|
|
166
192
|
* Note: Zalo returns a single update per call, not an array like Telegram
|
|
@@ -183,8 +209,8 @@ export async function setWebhook(
|
|
|
183
209
|
token: string,
|
|
184
210
|
params: ZaloSetWebhookParams,
|
|
185
211
|
fetcher?: ZaloFetch,
|
|
186
|
-
): Promise<ZaloApiResponse<
|
|
187
|
-
return callZaloApi<
|
|
212
|
+
): Promise<ZaloApiResponse<ZaloWebhookInfo>> {
|
|
213
|
+
return callZaloApi<ZaloWebhookInfo>("setWebhook", token, params, { fetch: fetcher });
|
|
188
214
|
}
|
|
189
215
|
|
|
190
216
|
/**
|
|
@@ -193,8 +219,12 @@ export async function setWebhook(
|
|
|
193
219
|
export async function deleteWebhook(
|
|
194
220
|
token: string,
|
|
195
221
|
fetcher?: ZaloFetch,
|
|
196
|
-
|
|
197
|
-
|
|
222
|
+
timeoutMs?: number,
|
|
223
|
+
): Promise<ZaloApiResponse<ZaloWebhookInfo>> {
|
|
224
|
+
return callZaloApi<ZaloWebhookInfo>("deleteWebhook", token, undefined, {
|
|
225
|
+
timeoutMs,
|
|
226
|
+
fetch: fetcher,
|
|
227
|
+
});
|
|
198
228
|
}
|
|
199
229
|
|
|
200
230
|
/**
|
|
@@ -203,6 +233,6 @@ export async function deleteWebhook(
|
|
|
203
233
|
export async function getWebhookInfo(
|
|
204
234
|
token: string,
|
|
205
235
|
fetcher?: ZaloFetch,
|
|
206
|
-
): Promise<ZaloApiResponse<
|
|
207
|
-
return callZaloApi("getWebhookInfo", token, undefined, { fetch: fetcher });
|
|
236
|
+
): Promise<ZaloApiResponse<ZaloWebhookInfo>> {
|
|
237
|
+
return callZaloApi<ZaloWebhookInfo>("getWebhookInfo", token, undefined, { fetch: fetcher });
|
|
208
238
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/zalo";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createStartAccountContext } from "../../test-utils/start-account-context.js";
|
|
4
|
+
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
5
|
+
|
|
6
|
+
const hoisted = vi.hoisted(() => ({
|
|
7
|
+
monitorZaloProvider: vi.fn(),
|
|
8
|
+
probeZalo: vi.fn(async () => ({
|
|
9
|
+
ok: false as const,
|
|
10
|
+
error: "probe failed",
|
|
11
|
+
elapsedMs: 1,
|
|
12
|
+
})),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("./monitor.js", async () => {
|
|
16
|
+
const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
monitorZaloProvider: hoisted.monitorZaloProvider,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock("./probe.js", async () => {
|
|
24
|
+
const actual = await vi.importActual<typeof import("./probe.js")>("./probe.js");
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
probeZalo: hoisted.probeZalo,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
import { zaloPlugin } from "./channel.js";
|
|
32
|
+
|
|
33
|
+
function buildAccount(): ResolvedZaloAccount {
|
|
34
|
+
return {
|
|
35
|
+
accountId: "default",
|
|
36
|
+
enabled: true,
|
|
37
|
+
token: "test-token",
|
|
38
|
+
tokenSource: "config",
|
|
39
|
+
config: {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("zaloPlugin gateway.startAccount", () => {
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps startAccount pending until abort", async () => {
|
|
49
|
+
hoisted.monitorZaloProvider.mockImplementationOnce(
|
|
50
|
+
async ({ abortSignal }: { abortSignal: AbortSignal }) =>
|
|
51
|
+
await new Promise<void>((resolve) => {
|
|
52
|
+
if (abortSignal.aborted) {
|
|
53
|
+
resolve();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const patches: ChannelAccountSnapshot[] = [];
|
|
61
|
+
const abort = new AbortController();
|
|
62
|
+
const task = zaloPlugin.gateway!.startAccount!(
|
|
63
|
+
createStartAccountContext({
|
|
64
|
+
account: buildAccount(),
|
|
65
|
+
abortSignal: abort.signal,
|
|
66
|
+
statusPatchSink: (next) => patches.push({ ...next }),
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let settled = false;
|
|
71
|
+
void task.then(() => {
|
|
72
|
+
settled = true;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await vi.waitFor(() => {
|
|
76
|
+
expect(hoisted.probeZalo).toHaveBeenCalledOnce();
|
|
77
|
+
expect(hoisted.monitorZaloProvider).toHaveBeenCalledOnce();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(settled).toBe(false);
|
|
81
|
+
expect(patches).toContainEqual(
|
|
82
|
+
expect.objectContaining({
|
|
83
|
+
accountId: "default",
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
abort.abort();
|
|
88
|
+
await task;
|
|
89
|
+
|
|
90
|
+
expect(settled).toBe(true);
|
|
91
|
+
expect(hoisted.monitorZaloProvider).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
token: "test-token",
|
|
94
|
+
account: expect.objectContaining({ accountId: "default" }),
|
|
95
|
+
abortSignal: abort.signal,
|
|
96
|
+
useWebhook: false,
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|