@openclaw/zalo 2026.5.2 → 2026.5.3-beta.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.
Files changed (87) hide show
  1. package/dist/accounts-9NLDDlZ8.js +118 -0
  2. package/dist/actions.runtime-kJ65ZxW7.js +5 -0
  3. package/dist/api.js +5 -0
  4. package/dist/channel-VPbtV3Oq.js +343 -0
  5. package/dist/channel-plugin-api.js +2 -0
  6. package/dist/channel.runtime-BnTAWQx5.js +106 -0
  7. package/dist/contract-api.js +3 -0
  8. package/dist/group-access-DZR43lOR.js +30 -0
  9. package/dist/index.js +22 -0
  10. package/dist/monitor-DMysJBWa.js +823 -0
  11. package/dist/monitor.webhook-DqnuvgjV.js +175 -0
  12. package/dist/proxy-CY8VuC6H.js +135 -0
  13. package/dist/runtime-BRFxnYQx.js +8 -0
  14. package/dist/runtime-api-MOTmRW4F.js +19 -0
  15. package/dist/runtime-api.js +3 -0
  16. package/dist/secret-contract-Dw93tGo2.js +87 -0
  17. package/dist/secret-contract-api.js +2 -0
  18. package/dist/send-Gv3l5EGI.js +101 -0
  19. package/dist/setup-api.js +30 -0
  20. package/dist/setup-core-DigRD3j1.js +166 -0
  21. package/dist/setup-entry.js +15 -0
  22. package/dist/setup-surface-2Up3yWov.js +216 -0
  23. package/dist/test-api.js +2 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -9
  26. package/channel-plugin-api.ts +0 -1
  27. package/contract-api.ts +0 -5
  28. package/index.test.ts +0 -15
  29. package/index.ts +0 -20
  30. package/runtime-api.test.ts +0 -17
  31. package/runtime-api.ts +0 -75
  32. package/secret-contract-api.ts +0 -5
  33. package/setup-api.ts +0 -34
  34. package/setup-entry.ts +0 -13
  35. package/src/accounts.test.ts +0 -70
  36. package/src/accounts.ts +0 -60
  37. package/src/actions.runtime.ts +0 -5
  38. package/src/actions.test.ts +0 -32
  39. package/src/actions.ts +0 -62
  40. package/src/api.test.ts +0 -149
  41. package/src/api.ts +0 -265
  42. package/src/approval-auth.test.ts +0 -17
  43. package/src/approval-auth.ts +0 -25
  44. package/src/channel.directory.test.ts +0 -59
  45. package/src/channel.runtime.ts +0 -93
  46. package/src/channel.startup.test.ts +0 -101
  47. package/src/channel.ts +0 -275
  48. package/src/config-schema.test.ts +0 -30
  49. package/src/config-schema.ts +0 -29
  50. package/src/group-access.ts +0 -49
  51. package/src/monitor.group-policy.test.ts +0 -94
  52. package/src/monitor.image.polling.test.ts +0 -110
  53. package/src/monitor.lifecycle.test.ts +0 -198
  54. package/src/monitor.pairing.lifecycle.test.ts +0 -141
  55. package/src/monitor.polling.media-reply.test.ts +0 -425
  56. package/src/monitor.reply-once.lifecycle.test.ts +0 -171
  57. package/src/monitor.ts +0 -1028
  58. package/src/monitor.types.ts +0 -4
  59. package/src/monitor.webhook.test.ts +0 -806
  60. package/src/monitor.webhook.ts +0 -278
  61. package/src/outbound-media.test.ts +0 -182
  62. package/src/outbound-media.ts +0 -241
  63. package/src/outbound-payload.contract.test.ts +0 -45
  64. package/src/probe.ts +0 -45
  65. package/src/proxy.ts +0 -24
  66. package/src/runtime-api.ts +0 -75
  67. package/src/runtime-support.ts +0 -91
  68. package/src/runtime.ts +0 -9
  69. package/src/secret-contract.ts +0 -109
  70. package/src/secret-input.ts +0 -5
  71. package/src/send.test.ts +0 -120
  72. package/src/send.ts +0 -153
  73. package/src/session-route.ts +0 -32
  74. package/src/setup-allow-from.ts +0 -94
  75. package/src/setup-core.ts +0 -149
  76. package/src/setup-status.test.ts +0 -33
  77. package/src/setup-surface.test.ts +0 -175
  78. package/src/setup-surface.ts +0 -291
  79. package/src/status-issues.test.ts +0 -17
  80. package/src/status-issues.ts +0 -37
  81. package/src/test-support/lifecycle-test-support.ts +0 -413
  82. package/src/test-support/monitor-mocks-test-support.ts +0 -209
  83. package/src/token.test.ts +0 -92
  84. package/src/token.ts +0 -79
  85. package/src/types.ts +0 -50
  86. package/test-api.ts +0 -1
  87. package/tsconfig.json +0 -16
@@ -1,413 +0,0 @@
1
- import { request as httpRequest } from "node:http";
2
- import { expect, vi } from "vitest";
3
- import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
4
- import type { ResolvedZaloAccount } from "../types.js";
5
-
6
- function resolveLifecycleAllowFrom(params: {
7
- dmPolicy: "open" | "pairing";
8
- allowFrom?: string[];
9
- }): string[] | undefined {
10
- return params.allowFrom ?? (params.dmPolicy === "open" ? ["*"] : undefined);
11
- }
12
-
13
- function createLifecycleConfig(params: {
14
- accountId: string;
15
- dmPolicy: "open" | "pairing";
16
- allowFrom?: string[];
17
- webhookUrl?: string;
18
- webhookSecret?: string;
19
- }): OpenClawConfig {
20
- const webhookUrl = params.webhookUrl ?? "https://example.com/hooks/zalo";
21
- const webhookSecret = params.webhookSecret ?? "supersecret";
22
- const allowFrom = resolveLifecycleAllowFrom(params);
23
- return {
24
- channels: {
25
- zalo: {
26
- enabled: true,
27
- accounts: {
28
- [params.accountId]: {
29
- enabled: true,
30
- webhookUrl,
31
- webhookSecret, // pragma: allowlist secret
32
- dmPolicy: params.dmPolicy,
33
- ...(allowFrom ? { allowFrom } : {}),
34
- },
35
- },
36
- },
37
- },
38
- } as OpenClawConfig;
39
- }
40
-
41
- function createLifecycleAccount(params: {
42
- accountId: string;
43
- dmPolicy: "open" | "pairing";
44
- allowFrom?: string[];
45
- webhookUrl?: string;
46
- webhookSecret?: string;
47
- }): ResolvedZaloAccount {
48
- const webhookUrl = params.webhookUrl ?? "https://example.com/hooks/zalo";
49
- const webhookSecret = params.webhookSecret ?? "supersecret";
50
- const allowFrom = resolveLifecycleAllowFrom(params);
51
- return {
52
- accountId: params.accountId,
53
- enabled: true,
54
- token: "zalo-token",
55
- tokenSource: "config",
56
- config: {
57
- webhookUrl,
58
- webhookSecret, // pragma: allowlist secret
59
- dmPolicy: params.dmPolicy,
60
- ...(allowFrom ? { allowFrom } : {}),
61
- },
62
- } as ResolvedZaloAccount;
63
- }
64
-
65
- export function createLifecycleMonitorSetup(params: {
66
- accountId: string;
67
- dmPolicy: "open" | "pairing";
68
- allowFrom?: string[];
69
- webhookUrl?: string;
70
- webhookSecret?: string;
71
- }) {
72
- return {
73
- account: createLifecycleAccount(params),
74
- config: createLifecycleConfig(params),
75
- };
76
- }
77
-
78
- export function createTextUpdate(params: {
79
- messageId: string;
80
- userId: string;
81
- userName: string;
82
- chatId: string;
83
- text?: string;
84
- }) {
85
- return {
86
- event_name: "message.text.received",
87
- message: {
88
- from: { id: params.userId, name: params.userName },
89
- chat: { id: params.chatId, chat_type: "PRIVATE" as const },
90
- message_id: params.messageId,
91
- date: Math.floor(Date.now() / 1000),
92
- text: params.text ?? "hello from zalo",
93
- },
94
- };
95
- }
96
-
97
- export function createImageUpdate(params?: {
98
- messageId?: string;
99
- userId?: string;
100
- displayName?: string;
101
- chatId?: string;
102
- photoUrl?: string;
103
- date?: number;
104
- }) {
105
- return {
106
- event_name: "message.image.received",
107
- message: {
108
- date: params?.date ?? 1774086023728,
109
- chat: { chat_type: "PRIVATE" as const, id: params?.chatId ?? "chat-123" },
110
- caption: "",
111
- message_id: params?.messageId ?? "msg-123",
112
- message_type: "CHAT_PHOTO",
113
- from: {
114
- id: params?.userId ?? "user-123",
115
- is_bot: false,
116
- display_name: params?.displayName ?? "Test User",
117
- },
118
- photo_url: params?.photoUrl ?? "https://example.com/test-image.jpg",
119
- },
120
- };
121
- }
122
-
123
- export function createImageLifecycleCore() {
124
- const finalizeInboundContextMock = vi.fn((ctx: Record<string, unknown>) => ctx);
125
- const buildChannelTurnContextMock = vi.fn(
126
- (params: {
127
- channel: string;
128
- accountId?: string;
129
- messageId?: string;
130
- timestamp?: number;
131
- from: string;
132
- sender: { id: string; name?: string };
133
- conversation: { kind: string; label?: string };
134
- route: {
135
- accountId?: string;
136
- routeSessionKey: string;
137
- dispatchSessionKey?: string;
138
- };
139
- reply: { to: string; originatingTo: string };
140
- message: { body?: string; rawBody: string; bodyForAgent?: string; commandBody?: string };
141
- media?: Array<{ path?: string; url?: string; contentType?: string }>;
142
- extra?: Record<string, unknown>;
143
- }) =>
144
- finalizeInboundContextMock({
145
- Body: params.message.body ?? params.message.rawBody,
146
- BodyForAgent: params.message.bodyForAgent ?? params.message.rawBody,
147
- RawBody: params.message.rawBody,
148
- CommandBody: params.message.commandBody ?? params.message.rawBody,
149
- From: params.from,
150
- To: params.reply.to,
151
- SessionKey: params.route.dispatchSessionKey ?? params.route.routeSessionKey,
152
- AccountId: params.route.accountId ?? params.accountId,
153
- ChatType: params.conversation.kind,
154
- ConversationLabel: params.conversation.label,
155
- SenderName: params.sender.name,
156
- SenderId: params.sender.id,
157
- Provider: params.channel,
158
- Surface: params.channel,
159
- MessageSid: params.messageId,
160
- Timestamp: params.timestamp,
161
- MediaPath: params.media?.[0]?.path,
162
- MediaType: params.media?.[0]?.contentType,
163
- MediaUrl: params.media?.[0]?.url ?? params.media?.[0]?.path,
164
- OriginatingChannel: params.channel,
165
- OriginatingTo: params.reply.originatingTo,
166
- ...params.extra,
167
- }),
168
- );
169
- const recordInboundSessionMock = vi.fn(async () => undefined);
170
- const fetchRemoteMediaMock = vi.fn(async () => ({
171
- buffer: Buffer.from("image-bytes"),
172
- contentType: "image/jpeg",
173
- }));
174
- const saveMediaBufferMock = vi.fn(async () => ({
175
- path: "/tmp/zalo-photo.jpg",
176
- contentType: "image/jpeg",
177
- }));
178
- const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
179
- const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
180
- const core = {
181
- logging: {
182
- shouldLogVerbose: vi.fn(
183
- () => false,
184
- ) as unknown as PluginRuntime["logging"]["shouldLogVerbose"],
185
- },
186
- channel: {
187
- pairing: {
188
- readAllowFromStore:
189
- readAllowFromStoreMock as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
190
- upsertPairingRequest:
191
- upsertPairingRequestMock as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
192
- },
193
- routing: {
194
- resolveAgentRoute: vi.fn(() => ({
195
- agentId: "main",
196
- accountId: "default",
197
- sessionKey: "agent:main:zalo:direct:chat-123",
198
- })) as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
199
- },
200
- session: {
201
- resolveStorePath: vi.fn(
202
- () => "/tmp/zalo-sessions.json",
203
- ) as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
204
- readSessionUpdatedAt: vi.fn(
205
- () => undefined,
206
- ) as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
207
- recordInboundSession:
208
- recordInboundSessionMock as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
209
- },
210
- text: {
211
- resolveMarkdownTableMode: vi.fn(
212
- () => "code",
213
- ) as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
214
- },
215
- media: {
216
- fetchRemoteMedia:
217
- fetchRemoteMediaMock as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
218
- saveMediaBuffer:
219
- saveMediaBufferMock as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
220
- },
221
- reply: {
222
- finalizeInboundContext:
223
- finalizeInboundContextMock as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
224
- resolveEnvelopeFormatOptions: vi.fn(() => ({
225
- template: "channel+name+time",
226
- })) as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
227
- formatAgentEnvelope: vi.fn(
228
- (opts: { body: string }) => opts.body,
229
- ) as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
230
- dispatchReplyWithBufferedBlockDispatcher: vi.fn(
231
- async () => undefined,
232
- ) as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
233
- },
234
- turn: {
235
- run: vi.fn(async (params: Parameters<PluginRuntime["channel"]["turn"]["run"]>[0]) => {
236
- const input = await params.adapter.ingest(params.raw);
237
- if (!input) {
238
- return {
239
- admission: { kind: "drop" as const, reason: "ingest-null" },
240
- dispatched: false,
241
- };
242
- }
243
- const resolved = await params.adapter.resolveTurn(
244
- input,
245
- {
246
- kind: "message",
247
- canStartAgentTurn: true,
248
- },
249
- {},
250
- );
251
- await resolved.recordInboundSession({
252
- storePath: resolved.storePath,
253
- sessionKey: resolved.ctxPayload.SessionKey ?? resolved.routeSessionKey,
254
- ctx: resolved.ctxPayload,
255
- groupResolution: resolved.record?.groupResolution,
256
- createIfMissing: resolved.record?.createIfMissing,
257
- updateLastRoute: resolved.record?.updateLastRoute,
258
- onRecordError: resolved.record?.onRecordError ?? (() => undefined),
259
- });
260
- if ("runDispatch" in resolved) {
261
- const dispatchResult = await resolved.runDispatch();
262
- return {
263
- admission: { kind: "dispatch" as const },
264
- dispatched: true,
265
- ctxPayload: resolved.ctxPayload,
266
- routeSessionKey: resolved.routeSessionKey,
267
- dispatchResult,
268
- };
269
- }
270
- const dispatchResult = await resolved.dispatchReplyWithBufferedBlockDispatcher({
271
- ctx: resolved.ctxPayload,
272
- cfg: resolved.cfg,
273
- dispatcherOptions: {
274
- ...resolved.dispatcherOptions,
275
- deliver: async (...args: Parameters<typeof resolved.delivery.deliver>) => {
276
- await resolved.delivery.deliver(...args);
277
- },
278
- onError: resolved.delivery.onError,
279
- },
280
- replyOptions: resolved.replyOptions,
281
- replyResolver: resolved.replyResolver,
282
- });
283
- return {
284
- admission: { kind: "dispatch" as const },
285
- dispatched: true,
286
- ctxPayload: resolved.ctxPayload,
287
- routeSessionKey: resolved.routeSessionKey,
288
- dispatchResult,
289
- };
290
- }) as unknown as PluginRuntime["channel"]["turn"]["run"],
291
- buildContext:
292
- buildChannelTurnContextMock as unknown as PluginRuntime["channel"]["turn"]["buildContext"],
293
- },
294
- commands: {
295
- shouldComputeCommandAuthorized: vi.fn(
296
- () => false,
297
- ) as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
298
- resolveCommandAuthorizedFromAuthorizers: vi.fn(
299
- () => false,
300
- ) as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
301
- isControlCommandMessage: vi.fn(
302
- () => false,
303
- ) as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
304
- },
305
- },
306
- } as PluginRuntime;
307
- return {
308
- core,
309
- finalizeInboundContextMock,
310
- recordInboundSessionMock,
311
- fetchRemoteMediaMock,
312
- saveMediaBufferMock,
313
- readAllowFromStoreMock,
314
- upsertPairingRequestMock,
315
- };
316
- }
317
-
318
- export function expectImageLifecycleDelivery(params: {
319
- fetchRemoteMediaMock: ReturnType<typeof vi.fn>;
320
- saveMediaBufferMock: ReturnType<typeof vi.fn>;
321
- finalizeInboundContextMock: ReturnType<typeof vi.fn>;
322
- recordInboundSessionMock: ReturnType<typeof vi.fn>;
323
- photoUrl?: string;
324
- senderName?: string;
325
- mediaPath?: string;
326
- mediaType?: string;
327
- }) {
328
- const photoUrl = params.photoUrl ?? "https://example.com/test-image.jpg";
329
- const senderName = params.senderName ?? "Test User";
330
- const mediaPath = params.mediaPath ?? "/tmp/zalo-photo.jpg";
331
- const mediaType = params.mediaType ?? "image/jpeg";
332
- expect(params.fetchRemoteMediaMock).toHaveBeenCalledWith({
333
- url: photoUrl,
334
- maxBytes: 5 * 1024 * 1024,
335
- });
336
- expect(params.saveMediaBufferMock).toHaveBeenCalledTimes(1);
337
- expect(params.finalizeInboundContextMock).toHaveBeenCalledWith(
338
- expect.objectContaining({
339
- SenderName: senderName,
340
- MediaPath: mediaPath,
341
- MediaType: mediaType,
342
- }),
343
- );
344
- expect(params.recordInboundSessionMock).toHaveBeenCalledWith(
345
- expect.objectContaining({
346
- ctx: expect.objectContaining({
347
- SenderName: senderName,
348
- MediaPath: mediaPath,
349
- MediaType: mediaType,
350
- }),
351
- }),
352
- );
353
- }
354
-
355
- export async function settleAsyncWork(): Promise<void> {
356
- for (let i = 0; i < 6; i += 1) {
357
- await Promise.resolve();
358
- await new Promise((resolve) => setTimeout(resolve, 0));
359
- }
360
- }
361
-
362
- async function postWebhookUpdate(params: {
363
- baseUrl: string;
364
- path: string;
365
- secret: string;
366
- payload: Record<string, unknown>;
367
- }) {
368
- const url = new URL(params.path, params.baseUrl);
369
- const body = JSON.stringify(params.payload);
370
- return await new Promise<{ status: number; body: string }>((resolve, reject) => {
371
- const req = httpRequest(
372
- url,
373
- {
374
- method: "POST",
375
- headers: {
376
- "content-type": "application/json",
377
- "content-length": Buffer.byteLength(body),
378
- "x-bot-api-secret-token": params.secret,
379
- },
380
- },
381
- (res) => {
382
- const chunks: Buffer[] = [];
383
- res.on("data", (chunk) => {
384
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
385
- });
386
- res.on("end", () => {
387
- resolve({
388
- status: res.statusCode ?? 0,
389
- body: Buffer.concat(chunks).toString("utf8"),
390
- });
391
- });
392
- },
393
- );
394
- req.on("error", reject);
395
- req.write(body);
396
- req.end();
397
- });
398
- }
399
-
400
- export async function postWebhookReplay(params: {
401
- baseUrl: string;
402
- path: string;
403
- secret: string;
404
- payload: Record<string, unknown>;
405
- settleBeforeReplay?: boolean;
406
- }) {
407
- const first = await postWebhookUpdate(params);
408
- if (params.settleBeforeReplay) {
409
- await settleAsyncWork();
410
- }
411
- const replay = await postWebhookUpdate(params);
412
- return { first, replay };
413
- }
@@ -1,209 +0,0 @@
1
- import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers";
2
- import {
3
- createEmptyPluginRegistry,
4
- createRuntimeEnv,
5
- setActivePluginRegistry,
6
- } from "openclaw/plugin-sdk/plugin-test-runtime";
7
- import { vi, type Mock } from "vitest";
8
- import type { OpenClawConfig } from "../runtime-api.js";
9
- import type { ResolvedZaloAccount } from "../types.js";
10
-
11
- type MonitorModule = typeof import("../monitor.js");
12
- type SecretInputModule = typeof import("../secret-input.js");
13
- type WebhookModule = typeof import("../monitor.webhook.js");
14
-
15
- const monitorModuleUrl = new URL("../monitor.ts", import.meta.url).href;
16
- const secretInputModuleUrl = new URL("../secret-input.ts", import.meta.url).href;
17
- const webhookModuleUrl = new URL("../monitor.webhook.ts", import.meta.url).href;
18
- const apiModuleId = new URL("../api.js", import.meta.url).pathname;
19
- const runtimeModuleId = new URL("../runtime.js", import.meta.url).pathname;
20
-
21
- type UnknownMock = Mock<(...args: unknown[]) => unknown>;
22
- type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise<unknown>>;
23
- const loadedMonitorModules = new Set<MonitorModule>();
24
- const cachedMonitorModules = new Map<string, Promise<MonitorModule>>();
25
- let cachedWebhookModule: Promise<WebhookModule> | undefined;
26
-
27
- type ZaloLifecycleMocks = {
28
- setWebhookMock: AsyncUnknownMock;
29
- deleteWebhookMock: AsyncUnknownMock;
30
- getWebhookInfoMock: AsyncUnknownMock;
31
- getUpdatesMock: UnknownMock;
32
- sendChatActionMock: AsyncUnknownMock;
33
- sendMessageMock: AsyncUnknownMock;
34
- sendPhotoMock: AsyncUnknownMock;
35
- getZaloRuntimeMock: UnknownMock;
36
- };
37
-
38
- const lifecycleMocks = vi.hoisted(
39
- (): ZaloLifecycleMocks => ({
40
- setWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })),
41
- deleteWebhookMock: vi.fn(async () => ({ ok: true, result: { url: "" } })),
42
- getWebhookInfoMock: vi.fn(async () => ({ ok: true, result: { url: "" } })),
43
- getUpdatesMock: vi.fn(() => new Promise(() => {})),
44
- sendChatActionMock: vi.fn(async () => ({ ok: true })),
45
- sendMessageMock: vi.fn(async () => ({
46
- ok: true,
47
- result: { message_id: "zalo-test-reply-1" },
48
- })),
49
- sendPhotoMock: vi.fn(async () => ({ ok: true })),
50
- getZaloRuntimeMock: vi.fn(),
51
- }),
52
- );
53
-
54
- const setWebhookMock = lifecycleMocks.setWebhookMock;
55
- export const getUpdatesMock = lifecycleMocks.getUpdatesMock;
56
- export const sendMessageMock = lifecycleMocks.sendMessageMock;
57
- export const sendPhotoMock = lifecycleMocks.sendPhotoMock;
58
- export const getZaloRuntimeMock: UnknownMock = lifecycleMocks.getZaloRuntimeMock;
59
-
60
- function installLifecycleModuleMocks() {
61
- vi.doMock(apiModuleId, async () => {
62
- const actual = await vi.importActual<object>(apiModuleId);
63
- return {
64
- ...actual,
65
- deleteWebhook: lifecycleMocks.deleteWebhookMock,
66
- getUpdates: lifecycleMocks.getUpdatesMock,
67
- getWebhookInfo: lifecycleMocks.getWebhookInfoMock,
68
- sendChatAction: lifecycleMocks.sendChatActionMock,
69
- sendMessage: lifecycleMocks.sendMessageMock,
70
- sendPhoto: lifecycleMocks.sendPhotoMock,
71
- setWebhook: lifecycleMocks.setWebhookMock,
72
- };
73
- });
74
-
75
- vi.doMock(runtimeModuleId, () => ({
76
- getZaloRuntime: lifecycleMocks.getZaloRuntimeMock,
77
- }));
78
- }
79
-
80
- async function importMonitorModule(params: {
81
- cacheBust: string;
82
- mocked: boolean;
83
- }): Promise<MonitorModule> {
84
- vi.resetModules();
85
- if (params.mocked) {
86
- installLifecycleModuleMocks();
87
- } else {
88
- vi.doUnmock(apiModuleId);
89
- vi.doUnmock(runtimeModuleId);
90
- }
91
- const module = (await import(
92
- `${monitorModuleUrl}?t=${params.cacheBust}-${Date.now()}`
93
- )) as MonitorModule;
94
- loadedMonitorModules.add(module);
95
- return module;
96
- }
97
-
98
- async function importSecretInputModule(cacheBust: string): Promise<SecretInputModule> {
99
- return (await import(
100
- `${secretInputModuleUrl}?t=${cacheBust}-${Date.now()}`
101
- )) as SecretInputModule;
102
- }
103
-
104
- async function importCachedWebhookModule(): Promise<WebhookModule> {
105
- cachedWebhookModule ??= import(webhookModuleUrl) as Promise<WebhookModule>;
106
- return await cachedWebhookModule;
107
- }
108
-
109
- export async function resetLifecycleTestState() {
110
- vi.clearAllMocks();
111
- (await importCachedWebhookModule()).clearZaloWebhookSecurityStateForTest();
112
- for (const module of loadedMonitorModules) {
113
- module.__testing.clearHostedMediaRouteRefsForTest();
114
- }
115
- setActivePluginRegistry(createEmptyPluginRegistry());
116
- }
117
-
118
- export function setLifecycleRuntimeCore(
119
- channel: NonNullable<NonNullable<Parameters<typeof createPluginRuntimeMock>[0]>["channel"]>,
120
- ) {
121
- getZaloRuntimeMock.mockReturnValue(
122
- createPluginRuntimeMock({
123
- channel,
124
- }),
125
- );
126
- }
127
-
128
- async function loadLifecycleMonitorModule(): Promise<MonitorModule> {
129
- return await importMonitorModule({ cacheBust: "monitor", mocked: true });
130
- }
131
-
132
- export async function loadCachedLifecycleMonitorModule(cacheKey: string): Promise<MonitorModule> {
133
- const key = cacheKey.trim();
134
- if (!key) {
135
- throw new Error("cacheKey is required");
136
- }
137
- const cached =
138
- cachedMonitorModules.get(key) ??
139
- (async () => {
140
- installLifecycleModuleMocks();
141
- const module = (await import(`${monitorModuleUrl}?t=${key}`)) as MonitorModule;
142
- loadedMonitorModules.add(module);
143
- return module;
144
- })();
145
- cachedMonitorModules.set(key, cached);
146
- return await cached;
147
- }
148
-
149
- export async function startWebhookLifecycleMonitor(params: {
150
- account: ResolvedZaloAccount;
151
- config: OpenClawConfig;
152
- token?: string;
153
- webhookUrl?: string;
154
- webhookSecret?: string;
155
- cacheKey?: string;
156
- }) {
157
- const registry = createEmptyPluginRegistry();
158
- setActivePluginRegistry(registry);
159
- const abort = new AbortController();
160
- const runtime = createRuntimeEnv();
161
- const accountWebhookUrl =
162
- typeof params.account.config?.webhookUrl === "string"
163
- ? params.account.config.webhookUrl
164
- : undefined;
165
- const webhookUrl = params.webhookUrl ?? accountWebhookUrl;
166
- const { normalizeSecretInputString } = await importSecretInputModule("secret-input");
167
- const webhookSecret =
168
- params.webhookSecret ?? normalizeSecretInputString(params.account.config?.webhookSecret);
169
- const { monitorZaloProvider } = params.cacheKey
170
- ? await loadCachedLifecycleMonitorModule(params.cacheKey)
171
- : await loadLifecycleMonitorModule();
172
- const run = monitorZaloProvider({
173
- token: params.token ?? "zalo-token",
174
- account: params.account,
175
- config: params.config,
176
- runtime,
177
- abortSignal: abort.signal,
178
- useWebhook: true,
179
- webhookUrl,
180
- webhookSecret,
181
- });
182
-
183
- await vi.waitFor(() => {
184
- const webhookRoute = registry.httpRoutes.find((route) => route.source === "zalo-webhook");
185
- const hostedMediaRoute = registry.httpRoutes.find(
186
- (route) => route.source === "zalo-hosted-media",
187
- );
188
- if (setWebhookMock.mock.calls.length !== 1 || !webhookRoute || !hostedMediaRoute) {
189
- throw new Error("waiting for webhook registration");
190
- }
191
- });
192
-
193
- const route = registry.httpRoutes.find((entry) => entry.source === "zalo-webhook");
194
- if (!route) {
195
- throw new Error("missing plugin HTTP route");
196
- }
197
-
198
- return {
199
- abort,
200
- registry,
201
- route,
202
- run,
203
- runtime,
204
- stop: async () => {
205
- abort.abort();
206
- await run;
207
- },
208
- };
209
- }