@openclaw/zalo 2026.3.13 → 2026.5.2-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/README.md +1 -1
- package/api.ts +9 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +15 -0
- package/index.ts +16 -13
- package/openclaw.plugin.json +514 -1
- package/package.json +31 -5
- package/runtime-api.test.ts +17 -0
- package/runtime-api.ts +75 -0
- package/secret-contract-api.ts +5 -0
- package/setup-api.ts +34 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +70 -0
- package/src/accounts.ts +19 -19
- package/src/actions.runtime.ts +5 -0
- package/src/actions.test.ts +32 -0
- package/src/actions.ts +20 -14
- package/src/api.test.ts +93 -2
- package/src/api.ts +29 -2
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +25 -0
- package/src/channel.directory.test.ts +19 -6
- package/src/channel.runtime.ts +93 -0
- package/src/channel.startup.test.ts +26 -19
- package/src/channel.ts +229 -336
- package/src/config-schema.ts +3 -3
- package/src/group-access.ts +4 -3
- package/src/monitor.group-policy.test.ts +0 -12
- package/src/monitor.image.polling.test.ts +110 -0
- package/src/monitor.lifecycle.test.ts +41 -22
- package/src/monitor.pairing.lifecycle.test.ts +141 -0
- package/src/monitor.polling.media-reply.test.ts +425 -0
- package/src/monitor.reply-once.lifecycle.test.ts +171 -0
- package/src/monitor.ts +460 -206
- package/src/monitor.types.ts +4 -0
- package/src/monitor.webhook.test.ts +392 -62
- package/src/monitor.webhook.ts +73 -36
- package/src/outbound-media.test.ts +182 -0
- package/src/outbound-media.ts +241 -0
- package/src/outbound-payload.contract.test.ts +45 -0
- package/src/probe.ts +1 -1
- package/src/proxy.ts +1 -1
- package/src/runtime-api.ts +75 -0
- package/src/runtime-support.ts +91 -0
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +109 -0
- package/src/secret-input.ts +1 -9
- package/src/send.test.ts +120 -0
- package/src/send.ts +15 -13
- package/src/session-route.ts +32 -0
- package/src/setup-allow-from.ts +94 -0
- package/src/setup-core.ts +149 -0
- package/src/{onboarding.status.test.ts → setup-status.test.ts} +13 -4
- package/src/setup-surface.test.ts +175 -0
- package/src/{onboarding.ts → setup-surface.ts} +59 -177
- package/src/status-issues.test.ts +2 -14
- package/src/status-issues.ts +8 -2
- package/src/test-support/lifecycle-test-support.ts +413 -0
- package/src/test-support/monitor-mocks-test-support.ts +209 -0
- package/src/token.test.ts +15 -0
- package/src/token.ts +8 -17
- package/src/types.ts +2 -2
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/channel.sendpayload.test.ts +0 -44
package/src/monitor.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
OutboundReplyPayload,
|
|
6
|
-
} from "openclaw/plugin-sdk/zalo";
|
|
2
|
+
import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
|
|
3
|
+
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
|
4
|
+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
7
5
|
import {
|
|
8
|
-
createTypingCallbacks,
|
|
9
|
-
createScopedPairingAccess,
|
|
10
|
-
createReplyPrefixOptions,
|
|
11
|
-
issuePairingChallenge,
|
|
12
|
-
logTypingFailure,
|
|
13
6
|
resolveDirectDmAuthorizationOutcome,
|
|
14
7
|
resolveSenderCommandAuthorizationWithRuntime,
|
|
15
|
-
|
|
8
|
+
} from "openclaw/plugin-sdk/command-auth";
|
|
9
|
+
import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
10
|
+
import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
|
11
|
+
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
12
|
+
import {
|
|
13
|
+
deliverTextOrMediaReply,
|
|
14
|
+
type OutboundReplyPayload,
|
|
15
|
+
} from "openclaw/plugin-sdk/reply-payload";
|
|
16
|
+
import { waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
|
|
17
|
+
import {
|
|
16
18
|
resolveDefaultGroupPolicy,
|
|
17
|
-
resolveInboundRouteEnvelopeBuilderWithRuntime,
|
|
18
|
-
sendMediaWithLeadingCaption,
|
|
19
|
-
resolveWebhookPath,
|
|
20
|
-
waitForAbortSignal,
|
|
21
19
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
22
|
-
} from "openclaw/plugin-sdk/
|
|
20
|
+
} from "openclaw/plugin-sdk/runtime-group-policy";
|
|
21
|
+
import { registerPluginHttpRoute, resolveWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
23
22
|
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
24
23
|
import {
|
|
25
24
|
ZaloApiError,
|
|
@@ -39,21 +38,15 @@ import {
|
|
|
39
38
|
isZaloSenderAllowed,
|
|
40
39
|
resolveZaloRuntimeGroupPolicy,
|
|
41
40
|
} from "./group-access.js";
|
|
42
|
-
import {
|
|
43
|
-
clearZaloWebhookSecurityStateForTest,
|
|
44
|
-
getZaloWebhookRateLimitStateSizeForTest,
|
|
45
|
-
getZaloWebhookStatusCounterSizeForTest,
|
|
46
|
-
handleZaloWebhookRequest as handleZaloWebhookRequestInternal,
|
|
47
|
-
registerZaloWebhookTarget as registerZaloWebhookTargetInternal,
|
|
48
|
-
type ZaloWebhookTarget,
|
|
49
|
-
} from "./monitor.webhook.js";
|
|
50
41
|
import { resolveZaloProxyFetch } from "./proxy.js";
|
|
51
42
|
import { getZaloRuntime } from "./runtime.js";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
export type { ZaloRuntimeEnv } from "./monitor.types.js";
|
|
44
|
+
import type { ZaloRuntimeEnv } from "./monitor.types.js";
|
|
45
|
+
import {
|
|
46
|
+
prepareHostedZaloMediaUrl,
|
|
47
|
+
resolveHostedZaloMediaRoutePrefix,
|
|
48
|
+
tryHandleHostedZaloMediaRequest,
|
|
49
|
+
} from "./outbound-media.js";
|
|
57
50
|
|
|
58
51
|
export type ZaloMonitorOptions = {
|
|
59
52
|
token: string;
|
|
@@ -76,33 +69,113 @@ const ZALO_TYPING_TIMEOUT_MS = 5_000;
|
|
|
76
69
|
|
|
77
70
|
type ZaloCoreRuntime = ReturnType<typeof getZaloRuntime>;
|
|
78
71
|
type ZaloStatusSink = (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
72
|
+
type ZaloWebhookModule = typeof import("./monitor.webhook.js");
|
|
79
73
|
type ZaloProcessingContext = {
|
|
80
74
|
token: string;
|
|
81
75
|
account: ResolvedZaloAccount;
|
|
82
76
|
config: OpenClawConfig;
|
|
83
77
|
runtime: ZaloRuntimeEnv;
|
|
84
78
|
core: ZaloCoreRuntime;
|
|
79
|
+
mediaMaxMb: number;
|
|
80
|
+
canHostMedia: boolean;
|
|
81
|
+
webhookUrl?: string;
|
|
82
|
+
webhookPath?: string;
|
|
85
83
|
statusSink?: ZaloStatusSink;
|
|
86
84
|
fetcher?: ZaloFetch;
|
|
87
85
|
};
|
|
88
86
|
type ZaloPollingLoopParams = ZaloProcessingContext & {
|
|
89
87
|
abortSignal: AbortSignal;
|
|
90
88
|
isStopped: () => boolean;
|
|
91
|
-
mediaMaxMb: number;
|
|
92
89
|
};
|
|
93
90
|
type ZaloUpdateProcessingParams = ZaloProcessingContext & {
|
|
94
91
|
update: ZaloUpdate;
|
|
95
|
-
mediaMaxMb: number;
|
|
96
92
|
};
|
|
93
|
+
|
|
94
|
+
let zaloWebhookModulePromise: Promise<ZaloWebhookModule> | undefined;
|
|
95
|
+
const hostedMediaRouteRefs = new Map<string, { count: number; unregisters: Array<() => void> }>();
|
|
96
|
+
|
|
97
|
+
function loadZaloWebhookModule(): Promise<ZaloWebhookModule> {
|
|
98
|
+
zaloWebhookModulePromise ??= import("./monitor.webhook.js");
|
|
99
|
+
return zaloWebhookModulePromise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function registerSharedHostedMediaRoute(params: {
|
|
103
|
+
path: string;
|
|
104
|
+
accountId: string;
|
|
105
|
+
log?: (message: string) => void;
|
|
106
|
+
}): () => void {
|
|
107
|
+
const unregister = registerPluginHttpRoute({
|
|
108
|
+
auth: "plugin",
|
|
109
|
+
match: "prefix",
|
|
110
|
+
path: params.path,
|
|
111
|
+
pluginId: "zalo",
|
|
112
|
+
source: "zalo-hosted-media",
|
|
113
|
+
accountId: params.accountId,
|
|
114
|
+
log: params.log,
|
|
115
|
+
handler: async (req, res) => {
|
|
116
|
+
const handled = await tryHandleHostedZaloMediaRequest(req, res);
|
|
117
|
+
if (!handled && !res.headersSent) {
|
|
118
|
+
res.statusCode = 404;
|
|
119
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
120
|
+
res.end("Not Found");
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const existing = hostedMediaRouteRefs.get(params.path);
|
|
126
|
+
if (existing) {
|
|
127
|
+
existing.count += 1;
|
|
128
|
+
existing.unregisters.push(unregister);
|
|
129
|
+
return () => {
|
|
130
|
+
const current = hostedMediaRouteRefs.get(params.path);
|
|
131
|
+
if (!current) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (current.count > 1) {
|
|
135
|
+
current.count -= 1;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
hostedMediaRouteRefs.delete(params.path);
|
|
139
|
+
for (const unregisterHandle of current.unregisters) {
|
|
140
|
+
unregisterHandle();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
hostedMediaRouteRefs.set(params.path, { count: 1, unregisters: [unregister] });
|
|
146
|
+
return () => {
|
|
147
|
+
const current = hostedMediaRouteRefs.get(params.path);
|
|
148
|
+
if (!current) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (current.count > 1) {
|
|
152
|
+
current.count -= 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
hostedMediaRouteRefs.delete(params.path);
|
|
156
|
+
for (const unregisterHandle of current.unregisters) {
|
|
157
|
+
unregisterHandle();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
97
162
|
type ZaloMessagePipelineParams = ZaloProcessingContext & {
|
|
98
163
|
message: ZaloMessage;
|
|
99
164
|
text?: string;
|
|
100
165
|
mediaPath?: string;
|
|
101
166
|
mediaType?: string;
|
|
167
|
+
authorization?: ZaloMessageAuthorizationResult;
|
|
102
168
|
};
|
|
103
169
|
type ZaloImageMessageParams = ZaloProcessingContext & {
|
|
104
170
|
message: ZaloMessage;
|
|
105
|
-
|
|
171
|
+
};
|
|
172
|
+
type ZaloMessageAuthorizationResult = {
|
|
173
|
+
chatId: string;
|
|
174
|
+
commandAuthorized: boolean | undefined;
|
|
175
|
+
isGroup: boolean;
|
|
176
|
+
rawBody: string;
|
|
177
|
+
senderId: string;
|
|
178
|
+
senderName: string | undefined;
|
|
106
179
|
};
|
|
107
180
|
|
|
108
181
|
function formatZaloError(error: unknown): string {
|
|
@@ -132,38 +205,13 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str
|
|
|
132
205
|
}
|
|
133
206
|
}
|
|
134
207
|
|
|
135
|
-
export function registerZaloWebhookTarget(target: ZaloWebhookTarget): () => void {
|
|
136
|
-
return registerZaloWebhookTargetInternal(target, {
|
|
137
|
-
route: {
|
|
138
|
-
auth: "plugin",
|
|
139
|
-
match: "exact",
|
|
140
|
-
pluginId: "zalo",
|
|
141
|
-
source: "zalo-webhook",
|
|
142
|
-
accountId: target.account.accountId,
|
|
143
|
-
log: target.runtime.log,
|
|
144
|
-
handler: async (req, res) => {
|
|
145
|
-
const handled = await handleZaloWebhookRequest(req, res);
|
|
146
|
-
if (!handled && !res.headersSent) {
|
|
147
|
-
res.statusCode = 404;
|
|
148
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
149
|
-
res.end("Not Found");
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export {
|
|
157
|
-
clearZaloWebhookSecurityStateForTest,
|
|
158
|
-
getZaloWebhookRateLimitStateSizeForTest,
|
|
159
|
-
getZaloWebhookStatusCounterSizeForTest,
|
|
160
|
-
};
|
|
161
|
-
|
|
162
208
|
export async function handleZaloWebhookRequest(
|
|
163
209
|
req: IncomingMessage,
|
|
164
210
|
res: ServerResponse,
|
|
165
211
|
): Promise<boolean> {
|
|
166
|
-
|
|
212
|
+
const { handleZaloWebhookRequest: handleZaloWebhookRequestInternal } =
|
|
213
|
+
await loadZaloWebhookModule();
|
|
214
|
+
return await handleZaloWebhookRequestInternal(req, res, async ({ update, target }) => {
|
|
167
215
|
await processUpdate({
|
|
168
216
|
update,
|
|
169
217
|
token: target.token,
|
|
@@ -172,6 +220,9 @@ export async function handleZaloWebhookRequest(
|
|
|
172
220
|
runtime: target.runtime,
|
|
173
221
|
core: target.core as ZaloCoreRuntime,
|
|
174
222
|
mediaMaxMb: target.mediaMaxMb,
|
|
223
|
+
canHostMedia: target.canHostMedia,
|
|
224
|
+
webhookUrl: target.webhookUrl,
|
|
225
|
+
webhookPath: target.webhookPath,
|
|
175
226
|
statusSink: target.statusSink,
|
|
176
227
|
fetcher: target.fetcher,
|
|
177
228
|
});
|
|
@@ -185,9 +236,12 @@ function startPollingLoop(params: ZaloPollingLoopParams) {
|
|
|
185
236
|
config,
|
|
186
237
|
runtime,
|
|
187
238
|
core,
|
|
239
|
+
mediaMaxMb,
|
|
240
|
+
canHostMedia,
|
|
241
|
+
webhookUrl,
|
|
242
|
+
webhookPath,
|
|
188
243
|
abortSignal,
|
|
189
244
|
isStopped,
|
|
190
|
-
mediaMaxMb,
|
|
191
245
|
statusSink,
|
|
192
246
|
fetcher,
|
|
193
247
|
} = params;
|
|
@@ -199,19 +253,25 @@ function startPollingLoop(params: ZaloPollingLoopParams) {
|
|
|
199
253
|
runtime,
|
|
200
254
|
core,
|
|
201
255
|
mediaMaxMb,
|
|
256
|
+
canHostMedia,
|
|
257
|
+
webhookUrl,
|
|
258
|
+
webhookPath,
|
|
202
259
|
statusSink,
|
|
203
260
|
fetcher,
|
|
204
261
|
};
|
|
205
262
|
|
|
206
263
|
runtime.log?.(`[${account.accountId}] Zalo polling loop started timeout=${String(pollTimeout)}s`);
|
|
207
264
|
|
|
208
|
-
const poll = async () => {
|
|
265
|
+
const poll = async (): Promise<void> => {
|
|
209
266
|
if (isStopped() || abortSignal.aborted) {
|
|
210
|
-
return;
|
|
267
|
+
return undefined;
|
|
211
268
|
}
|
|
212
269
|
|
|
213
270
|
try {
|
|
214
271
|
const response = await getUpdates(token, { timeout: pollTimeout }, fetcher);
|
|
272
|
+
if (isStopped() || abortSignal.aborted) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
215
275
|
if (response.ok && response.result) {
|
|
216
276
|
statusSink?.({ lastInboundAt: Date.now() });
|
|
217
277
|
await processUpdate({
|
|
@@ -239,9 +299,21 @@ function startPollingLoop(params: ZaloPollingLoopParams) {
|
|
|
239
299
|
async function processUpdate(params: ZaloUpdateProcessingParams): Promise<void> {
|
|
240
300
|
const { update, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher } = params;
|
|
241
301
|
const { event_name, message } = update;
|
|
242
|
-
const sharedContext = {
|
|
302
|
+
const sharedContext = {
|
|
303
|
+
token,
|
|
304
|
+
account,
|
|
305
|
+
config,
|
|
306
|
+
runtime,
|
|
307
|
+
core,
|
|
308
|
+
mediaMaxMb,
|
|
309
|
+
canHostMedia: params.canHostMedia,
|
|
310
|
+
webhookUrl: params.webhookUrl,
|
|
311
|
+
webhookPath: params.webhookPath,
|
|
312
|
+
statusSink,
|
|
313
|
+
fetcher,
|
|
314
|
+
};
|
|
243
315
|
if (!message) {
|
|
244
|
-
return;
|
|
316
|
+
return undefined;
|
|
245
317
|
}
|
|
246
318
|
|
|
247
319
|
switch (event_name) {
|
|
@@ -277,7 +349,7 @@ async function handleTextMessage(
|
|
|
277
349
|
const { message } = params;
|
|
278
350
|
const { text } = message;
|
|
279
351
|
if (!text?.trim()) {
|
|
280
|
-
return;
|
|
352
|
+
return undefined;
|
|
281
353
|
}
|
|
282
354
|
|
|
283
355
|
await processMessageWithPipeline({
|
|
@@ -290,15 +362,25 @@ async function handleTextMessage(
|
|
|
290
362
|
|
|
291
363
|
async function handleImageMessage(params: ZaloImageMessageParams): Promise<void> {
|
|
292
364
|
const { message, mediaMaxMb, account, core, runtime } = params;
|
|
293
|
-
const {
|
|
365
|
+
const { photo_url, caption } = message;
|
|
366
|
+
const authorization = await authorizeZaloMessage({
|
|
367
|
+
...params,
|
|
368
|
+
text: caption,
|
|
369
|
+
// Use a sentinel so auth sees this as an inbound image before the download happens.
|
|
370
|
+
mediaPath: photo_url ? "__pending_media__" : undefined,
|
|
371
|
+
mediaType: undefined,
|
|
372
|
+
});
|
|
373
|
+
if (!authorization) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
294
376
|
|
|
295
377
|
let mediaPath: string | undefined;
|
|
296
378
|
let mediaType: string | undefined;
|
|
297
379
|
|
|
298
|
-
if (
|
|
380
|
+
if (photo_url) {
|
|
299
381
|
try {
|
|
300
382
|
const maxBytes = mediaMaxMb * 1024 * 1024;
|
|
301
|
-
const fetched = await core.channel.media.fetchRemoteMedia({ url:
|
|
383
|
+
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo_url, maxBytes });
|
|
302
384
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
303
385
|
fetched.buffer,
|
|
304
386
|
fetched.contentType,
|
|
@@ -314,37 +396,29 @@ async function handleImageMessage(params: ZaloImageMessageParams): Promise<void>
|
|
|
314
396
|
|
|
315
397
|
await processMessageWithPipeline({
|
|
316
398
|
...params,
|
|
399
|
+
authorization,
|
|
317
400
|
text: caption,
|
|
318
401
|
mediaPath,
|
|
319
402
|
mediaType,
|
|
320
403
|
});
|
|
321
404
|
}
|
|
322
405
|
|
|
323
|
-
async function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
runtime,
|
|
330
|
-
core,
|
|
331
|
-
text,
|
|
332
|
-
mediaPath,
|
|
333
|
-
mediaType,
|
|
334
|
-
statusSink,
|
|
335
|
-
fetcher,
|
|
336
|
-
} = params;
|
|
337
|
-
const pairing = createScopedPairingAccess({
|
|
406
|
+
async function authorizeZaloMessage(
|
|
407
|
+
params: ZaloMessagePipelineParams,
|
|
408
|
+
): Promise<ZaloMessageAuthorizationResult | undefined> {
|
|
409
|
+
const { message, account, config, runtime, core, text, mediaPath, token, statusSink, fetcher } =
|
|
410
|
+
params;
|
|
411
|
+
const pairing = createChannelPairingController({
|
|
338
412
|
core,
|
|
339
413
|
channel: "zalo",
|
|
340
414
|
accountId: account.accountId,
|
|
341
415
|
});
|
|
342
|
-
const { from, chat
|
|
416
|
+
const { from, chat } = message;
|
|
343
417
|
|
|
344
418
|
const isGroup = chat.chat_type === "GROUP";
|
|
345
419
|
const chatId = chat.id;
|
|
346
420
|
const senderId = from.id;
|
|
347
|
-
const senderName = from.name;
|
|
421
|
+
const senderName = from.display_name ?? from.name;
|
|
348
422
|
|
|
349
423
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
350
424
|
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
|
|
@@ -380,7 +454,7 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
|
|
|
380
454
|
} else if (groupAccess.reason === "sender_not_allowlisted") {
|
|
381
455
|
logVerbose(core, runtime, `zalo: drop group sender ${senderId} (groupPolicy=allowlist)`);
|
|
382
456
|
}
|
|
383
|
-
return;
|
|
457
|
+
return undefined;
|
|
384
458
|
}
|
|
385
459
|
}
|
|
386
460
|
|
|
@@ -395,6 +469,8 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
|
|
|
395
469
|
configuredGroupAllowFrom: groupAllowFrom,
|
|
396
470
|
senderId,
|
|
397
471
|
isSenderAllowed: isZaloSenderAllowed,
|
|
472
|
+
channel: "zalo",
|
|
473
|
+
accountId: account.accountId,
|
|
398
474
|
readAllowFromStore: pairing.readAllowFromStore,
|
|
399
475
|
runtime: core.channel.commands,
|
|
400
476
|
});
|
|
@@ -406,16 +482,14 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
|
|
|
406
482
|
});
|
|
407
483
|
if (directDmOutcome === "disabled") {
|
|
408
484
|
logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
|
|
409
|
-
return;
|
|
485
|
+
return undefined;
|
|
410
486
|
}
|
|
411
487
|
if (directDmOutcome === "unauthorized") {
|
|
412
488
|
if (dmPolicy === "pairing") {
|
|
413
|
-
await
|
|
414
|
-
channel: "zalo",
|
|
489
|
+
await pairing.issueChallenge({
|
|
415
490
|
senderId,
|
|
416
491
|
senderIdLine: `Your Zalo user id: ${senderId}`,
|
|
417
492
|
meta: { name: senderName ?? undefined },
|
|
418
|
-
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
419
493
|
onCreated: () => {
|
|
420
494
|
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
|
|
421
495
|
},
|
|
@@ -441,8 +515,45 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
|
|
|
441
515
|
`Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`,
|
|
442
516
|
);
|
|
443
517
|
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
chatId,
|
|
523
|
+
commandAuthorized,
|
|
524
|
+
isGroup,
|
|
525
|
+
rawBody,
|
|
526
|
+
senderId,
|
|
527
|
+
senderName,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Promise<void> {
|
|
532
|
+
const {
|
|
533
|
+
message,
|
|
534
|
+
token,
|
|
535
|
+
account,
|
|
536
|
+
config,
|
|
537
|
+
runtime,
|
|
538
|
+
core,
|
|
539
|
+
mediaPath,
|
|
540
|
+
mediaType,
|
|
541
|
+
statusSink,
|
|
542
|
+
fetcher,
|
|
543
|
+
authorization: authorizationOverride,
|
|
544
|
+
} = params;
|
|
545
|
+
const { message_id, date } = message;
|
|
546
|
+
const authorization =
|
|
547
|
+
authorizationOverride ??
|
|
548
|
+
(await authorizeZaloMessage({
|
|
549
|
+
...params,
|
|
550
|
+
mediaPath,
|
|
551
|
+
mediaType,
|
|
552
|
+
}));
|
|
553
|
+
if (!authorization) {
|
|
444
554
|
return;
|
|
445
555
|
}
|
|
556
|
+
const { isGroup, chatId, senderId, senderName, rawBody, commandAuthorized } = authorization;
|
|
446
557
|
|
|
447
558
|
const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
448
559
|
cfg: config,
|
|
@@ -473,36 +584,54 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
|
|
|
473
584
|
body: rawBody,
|
|
474
585
|
});
|
|
475
586
|
|
|
476
|
-
const ctxPayload = core.channel.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
587
|
+
const ctxPayload = core.channel.turn.buildContext({
|
|
588
|
+
channel: "zalo",
|
|
589
|
+
accountId: route.accountId,
|
|
590
|
+
messageId: message_id,
|
|
591
|
+
timestamp: date ? date * 1000 : undefined,
|
|
592
|
+
from: isGroup ? `zalo:group:${chatId}` : `zalo:${senderId}`,
|
|
593
|
+
sender: {
|
|
594
|
+
id: senderId,
|
|
595
|
+
name: senderName || undefined,
|
|
596
|
+
},
|
|
597
|
+
conversation: {
|
|
598
|
+
kind: isGroup ? "group" : "direct",
|
|
599
|
+
id: chatId,
|
|
600
|
+
label: fromLabel,
|
|
601
|
+
routePeer: {
|
|
602
|
+
kind: isGroup ? "group" : "direct",
|
|
603
|
+
id: chatId,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
route: {
|
|
607
|
+
agentId: route.agentId,
|
|
608
|
+
accountId: route.accountId,
|
|
609
|
+
routeSessionKey: route.sessionKey,
|
|
610
|
+
},
|
|
611
|
+
reply: {
|
|
612
|
+
to: `zalo:${chatId}`,
|
|
613
|
+
originatingTo: `zalo:${chatId}`,
|
|
614
|
+
},
|
|
615
|
+
message: {
|
|
616
|
+
body,
|
|
617
|
+
bodyForAgent: rawBody,
|
|
618
|
+
rawBody,
|
|
619
|
+
commandBody: rawBody,
|
|
620
|
+
envelopeFrom: fromLabel,
|
|
621
|
+
},
|
|
622
|
+
media:
|
|
623
|
+
mediaPath || mediaType
|
|
624
|
+
? [
|
|
625
|
+
{
|
|
626
|
+
path: mediaPath,
|
|
627
|
+
url: mediaPath,
|
|
628
|
+
contentType: mediaType,
|
|
629
|
+
},
|
|
630
|
+
]
|
|
631
|
+
: undefined,
|
|
632
|
+
extra: {
|
|
633
|
+
CommandAuthorized: commandAuthorized,
|
|
634
|
+
GroupSubject: undefined,
|
|
506
635
|
},
|
|
507
636
|
});
|
|
508
637
|
|
|
@@ -511,61 +640,95 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr
|
|
|
511
640
|
channel: "zalo",
|
|
512
641
|
accountId: account.accountId,
|
|
513
642
|
});
|
|
514
|
-
const { onModelSelected, ...
|
|
643
|
+
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
|
|
515
644
|
cfg: config,
|
|
516
645
|
agentId: route.agentId,
|
|
517
646
|
channel: "zalo",
|
|
518
647
|
accountId: account.accountId,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
await sendChatAction(
|
|
523
|
-
token,
|
|
524
|
-
{
|
|
525
|
-
chat_id: chatId,
|
|
526
|
-
action: "typing",
|
|
527
|
-
},
|
|
528
|
-
fetcher,
|
|
529
|
-
ZALO_TYPING_TIMEOUT_MS,
|
|
530
|
-
);
|
|
531
|
-
},
|
|
532
|
-
onStartError: (err) => {
|
|
533
|
-
logTypingFailure({
|
|
534
|
-
log: (message) => logVerbose(core, runtime, message),
|
|
535
|
-
channel: "zalo",
|
|
536
|
-
action: "start",
|
|
537
|
-
target: chatId,
|
|
538
|
-
error: err,
|
|
539
|
-
});
|
|
540
|
-
},
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
544
|
-
ctx: ctxPayload,
|
|
545
|
-
cfg: config,
|
|
546
|
-
dispatcherOptions: {
|
|
547
|
-
...prefixOptions,
|
|
548
|
-
typingCallbacks,
|
|
549
|
-
deliver: async (payload) => {
|
|
550
|
-
await deliverZaloReply({
|
|
551
|
-
payload,
|
|
648
|
+
typing: {
|
|
649
|
+
start: async () => {
|
|
650
|
+
await sendChatAction(
|
|
552
651
|
token,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
accountId: account.accountId,
|
|
558
|
-
statusSink,
|
|
652
|
+
{
|
|
653
|
+
chat_id: chatId,
|
|
654
|
+
action: "typing",
|
|
655
|
+
},
|
|
559
656
|
fetcher,
|
|
560
|
-
|
|
561
|
-
|
|
657
|
+
ZALO_TYPING_TIMEOUT_MS,
|
|
658
|
+
);
|
|
562
659
|
},
|
|
563
|
-
|
|
564
|
-
|
|
660
|
+
onStartError: (err) => {
|
|
661
|
+
logTypingFailure({
|
|
662
|
+
log: (message) => logVerbose(core, runtime, message),
|
|
663
|
+
channel: "zalo",
|
|
664
|
+
action: "start",
|
|
665
|
+
target: chatId,
|
|
666
|
+
error: err,
|
|
667
|
+
});
|
|
565
668
|
},
|
|
566
669
|
},
|
|
567
|
-
|
|
568
|
-
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await core.channel.turn.run({
|
|
673
|
+
channel: "zalo",
|
|
674
|
+
accountId: account.accountId,
|
|
675
|
+
raw: message,
|
|
676
|
+
adapter: {
|
|
677
|
+
ingest: () => ({
|
|
678
|
+
id: message_id,
|
|
679
|
+
timestamp: date ? date * 1000 : undefined,
|
|
680
|
+
rawText: rawBody,
|
|
681
|
+
textForAgent: rawBody,
|
|
682
|
+
textForCommands: rawBody,
|
|
683
|
+
raw: message,
|
|
684
|
+
}),
|
|
685
|
+
resolveTurn: () => ({
|
|
686
|
+
cfg: config,
|
|
687
|
+
channel: "zalo",
|
|
688
|
+
accountId: account.accountId,
|
|
689
|
+
agentId: route.agentId,
|
|
690
|
+
routeSessionKey: route.sessionKey,
|
|
691
|
+
storePath,
|
|
692
|
+
ctxPayload,
|
|
693
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
694
|
+
dispatchReplyWithBufferedBlockDispatcher:
|
|
695
|
+
core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
696
|
+
delivery: {
|
|
697
|
+
deliver: async (payload) => {
|
|
698
|
+
await deliverZaloReply({
|
|
699
|
+
payload,
|
|
700
|
+
token,
|
|
701
|
+
chatId,
|
|
702
|
+
runtime,
|
|
703
|
+
core,
|
|
704
|
+
config,
|
|
705
|
+
webhookUrl: params.webhookUrl,
|
|
706
|
+
webhookPath: params.webhookPath,
|
|
707
|
+
proxyUrl: account.config.proxy,
|
|
708
|
+
mediaMaxBytes: params.mediaMaxMb * 1024 * 1024,
|
|
709
|
+
canHostMedia: params.canHostMedia,
|
|
710
|
+
accountId: account.accountId,
|
|
711
|
+
statusSink,
|
|
712
|
+
fetcher,
|
|
713
|
+
tableMode,
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
onError: (err, info) => {
|
|
717
|
+
runtime.error?.(
|
|
718
|
+
`[${account.accountId}] Zalo ${info.kind} reply failed: ${String(err)}`,
|
|
719
|
+
);
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
dispatcherOptions: replyPipeline,
|
|
723
|
+
replyOptions: {
|
|
724
|
+
onModelSelected,
|
|
725
|
+
},
|
|
726
|
+
record: {
|
|
727
|
+
onRecordError: (err) => {
|
|
728
|
+
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
}),
|
|
569
732
|
},
|
|
570
733
|
});
|
|
571
734
|
}
|
|
@@ -577,41 +740,70 @@ async function deliverZaloReply(params: {
|
|
|
577
740
|
runtime: ZaloRuntimeEnv;
|
|
578
741
|
core: ZaloCoreRuntime;
|
|
579
742
|
config: OpenClawConfig;
|
|
743
|
+
webhookUrl?: string;
|
|
744
|
+
webhookPath?: string;
|
|
745
|
+
proxyUrl?: string;
|
|
746
|
+
mediaMaxBytes: number;
|
|
747
|
+
canHostMedia: boolean;
|
|
580
748
|
accountId?: string;
|
|
581
749
|
statusSink?: ZaloStatusSink;
|
|
582
750
|
fetcher?: ZaloFetch;
|
|
583
751
|
tableMode?: MarkdownTableMode;
|
|
584
752
|
}): Promise<void> {
|
|
585
|
-
const {
|
|
753
|
+
const {
|
|
754
|
+
payload,
|
|
755
|
+
token,
|
|
756
|
+
chatId,
|
|
757
|
+
runtime,
|
|
758
|
+
core,
|
|
759
|
+
config,
|
|
760
|
+
webhookUrl,
|
|
761
|
+
webhookPath,
|
|
762
|
+
proxyUrl,
|
|
763
|
+
mediaMaxBytes,
|
|
764
|
+
canHostMedia,
|
|
765
|
+
accountId,
|
|
766
|
+
statusSink,
|
|
767
|
+
fetcher,
|
|
768
|
+
} = params;
|
|
586
769
|
const tableMode = params.tableMode ?? "code";
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
mediaUrls: resolveOutboundMediaUrls(payload),
|
|
590
|
-
caption: text,
|
|
591
|
-
send: async ({ mediaUrl, caption }) => {
|
|
592
|
-
await sendPhoto(token, { chat_id: chatId, photo: mediaUrl, caption }, fetcher);
|
|
593
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
594
|
-
},
|
|
595
|
-
onError: (error) => {
|
|
596
|
-
runtime.error?.(`Zalo photo send failed: ${String(error)}`);
|
|
597
|
-
},
|
|
770
|
+
const reply = resolveSendableOutboundReplyParts(payload, {
|
|
771
|
+
text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
|
|
598
772
|
});
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
for (const chunk of chunks) {
|
|
773
|
+
const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
|
|
774
|
+
await deliverTextOrMediaReply({
|
|
775
|
+
payload,
|
|
776
|
+
text: reply.text,
|
|
777
|
+
chunkText: (value) =>
|
|
778
|
+
core.channel.text.chunkMarkdownTextWithMode(value, ZALO_TEXT_LIMIT, chunkMode),
|
|
779
|
+
sendText: async (chunk) => {
|
|
607
780
|
try {
|
|
608
781
|
await sendMessage(token, { chat_id: chatId, text: chunk }, fetcher);
|
|
609
782
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
610
783
|
} catch (err) {
|
|
611
784
|
runtime.error?.(`Zalo message send failed: ${String(err)}`);
|
|
612
785
|
}
|
|
613
|
-
}
|
|
614
|
-
|
|
786
|
+
},
|
|
787
|
+
sendMedia: async ({ mediaUrl, caption }) => {
|
|
788
|
+
const sendableMediaUrl =
|
|
789
|
+
canHostMedia && webhookUrl && webhookPath
|
|
790
|
+
? await prepareHostedZaloMediaUrl({
|
|
791
|
+
mediaUrl,
|
|
792
|
+
webhookUrl,
|
|
793
|
+
webhookPath,
|
|
794
|
+
maxBytes: mediaMaxBytes,
|
|
795
|
+
proxyUrl,
|
|
796
|
+
})
|
|
797
|
+
: mediaUrl;
|
|
798
|
+
await sendPhoto(token, { chat_id: chatId, photo: sendableMediaUrl, caption }, fetcher);
|
|
799
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
800
|
+
},
|
|
801
|
+
onMediaError: (error) => {
|
|
802
|
+
runtime.error?.(
|
|
803
|
+
`Zalo photo send failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
804
|
+
);
|
|
805
|
+
},
|
|
806
|
+
});
|
|
615
807
|
}
|
|
616
808
|
|
|
617
809
|
export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<void> {
|
|
@@ -633,6 +825,23 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
633
825
|
const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
|
634
826
|
const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
|
|
635
827
|
const mode = useWebhook ? "webhook" : "polling";
|
|
828
|
+
const effectiveWebhookUrl = normalizeWebhookUrl(webhookUrl ?? account.config.webhookUrl);
|
|
829
|
+
const effectiveWebhookPath =
|
|
830
|
+
effectiveWebhookUrl || webhookPath?.trim() || account.config.webhookPath?.trim()
|
|
831
|
+
? (resolveWebhookPath({
|
|
832
|
+
webhookPath: webhookPath ?? account.config.webhookPath,
|
|
833
|
+
webhookUrl: effectiveWebhookUrl,
|
|
834
|
+
defaultPath: null,
|
|
835
|
+
}) ?? undefined)
|
|
836
|
+
: undefined;
|
|
837
|
+
const canHostMedia = Boolean(effectiveWebhookUrl && effectiveWebhookPath);
|
|
838
|
+
const hostedMediaRoutePath =
|
|
839
|
+
canHostMedia && effectiveWebhookUrl
|
|
840
|
+
? resolveHostedZaloMediaRoutePrefix({
|
|
841
|
+
webhookUrl: effectiveWebhookUrl,
|
|
842
|
+
webhookPath: effectiveWebhookPath,
|
|
843
|
+
})
|
|
844
|
+
: undefined;
|
|
636
845
|
|
|
637
846
|
let stopped = false;
|
|
638
847
|
const stopHandlers: Array<() => void> = [];
|
|
@@ -647,32 +856,49 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
647
856
|
handler();
|
|
648
857
|
}
|
|
649
858
|
};
|
|
859
|
+
const stopOnAbort = () => {
|
|
860
|
+
if (!useWebhook) {
|
|
861
|
+
stop();
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
abortSignal.addEventListener("abort", stopOnAbort, { once: true });
|
|
650
866
|
|
|
651
867
|
runtime.log?.(
|
|
652
868
|
`[${account.accountId}] Zalo provider init mode=${mode} mediaMaxMb=${String(effectiveMediaMaxMb)}`,
|
|
653
869
|
);
|
|
654
870
|
|
|
655
871
|
try {
|
|
872
|
+
if (hostedMediaRoutePath) {
|
|
873
|
+
const unregisterHostedMediaRoute = registerSharedHostedMediaRoute({
|
|
874
|
+
path: hostedMediaRoutePath,
|
|
875
|
+
accountId: account.accountId,
|
|
876
|
+
log: runtime.log,
|
|
877
|
+
});
|
|
878
|
+
stopHandlers.push(unregisterHostedMediaRoute);
|
|
879
|
+
}
|
|
880
|
+
|
|
656
881
|
if (useWebhook) {
|
|
657
|
-
|
|
882
|
+
const { registerZaloWebhookTarget } = await loadZaloWebhookModule();
|
|
883
|
+
if (!effectiveWebhookUrl || !webhookSecret) {
|
|
658
884
|
throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
|
|
659
885
|
}
|
|
660
|
-
if (!
|
|
886
|
+
if (!effectiveWebhookUrl.startsWith("https://")) {
|
|
661
887
|
throw new Error("Zalo webhook URL must use HTTPS");
|
|
662
888
|
}
|
|
663
889
|
if (webhookSecret.length < 8 || webhookSecret.length > 256) {
|
|
664
890
|
throw new Error("Zalo webhook secret must be 8-256 characters");
|
|
665
891
|
}
|
|
666
892
|
|
|
667
|
-
const path =
|
|
893
|
+
const path = effectiveWebhookPath;
|
|
668
894
|
if (!path) {
|
|
669
895
|
throw new Error("Zalo webhookPath could not be derived");
|
|
670
896
|
}
|
|
671
897
|
|
|
672
898
|
runtime.log?.(
|
|
673
|
-
`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(
|
|
899
|
+
`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(effectiveWebhookUrl)}`,
|
|
674
900
|
);
|
|
675
|
-
await setWebhook(token, { url:
|
|
901
|
+
await setWebhook(token, { url: effectiveWebhookUrl, secret_token: webhookSecret }, fetcher);
|
|
676
902
|
let webhookCleanupPromise: Promise<void> | undefined;
|
|
677
903
|
cleanupWebhook = async () => {
|
|
678
904
|
if (!webhookCleanupPromise) {
|
|
@@ -694,18 +920,41 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
694
920
|
};
|
|
695
921
|
runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
|
|
696
922
|
|
|
697
|
-
const unregister = registerZaloWebhookTarget(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
923
|
+
const unregister = registerZaloWebhookTarget(
|
|
924
|
+
{
|
|
925
|
+
token,
|
|
926
|
+
account,
|
|
927
|
+
config,
|
|
928
|
+
runtime,
|
|
929
|
+
core,
|
|
930
|
+
path,
|
|
931
|
+
webhookUrl: effectiveWebhookUrl,
|
|
932
|
+
webhookPath: path,
|
|
933
|
+
secret: webhookSecret,
|
|
934
|
+
statusSink: (patch) => statusSink?.(patch),
|
|
935
|
+
mediaMaxMb: effectiveMediaMaxMb,
|
|
936
|
+
canHostMedia,
|
|
937
|
+
fetcher,
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
route: {
|
|
941
|
+
auth: "plugin",
|
|
942
|
+
match: "exact",
|
|
943
|
+
pluginId: "zalo",
|
|
944
|
+
source: "zalo-webhook",
|
|
945
|
+
accountId: account.accountId,
|
|
946
|
+
log: runtime.log,
|
|
947
|
+
handler: async (req, res) => {
|
|
948
|
+
const handled = await handleZaloWebhookRequest(req, res);
|
|
949
|
+
if (!handled && !res.headersSent) {
|
|
950
|
+
res.statusCode = 404;
|
|
951
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
952
|
+
res.end("Not Found");
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
);
|
|
709
958
|
stopHandlers.push(unregister);
|
|
710
959
|
await waitForAbortSignal(abortSignal);
|
|
711
960
|
return;
|
|
@@ -748,6 +997,9 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
748
997
|
config,
|
|
749
998
|
runtime,
|
|
750
999
|
core,
|
|
1000
|
+
canHostMedia,
|
|
1001
|
+
webhookUrl: effectiveWebhookUrl,
|
|
1002
|
+
webhookPath: effectiveWebhookPath,
|
|
751
1003
|
abortSignal,
|
|
752
1004
|
isStopped: () => stopped,
|
|
753
1005
|
mediaMaxMb: effectiveMediaMaxMb,
|
|
@@ -762,6 +1014,7 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
762
1014
|
);
|
|
763
1015
|
throw err;
|
|
764
1016
|
} finally {
|
|
1017
|
+
abortSignal.removeEventListener("abort", stopOnAbort);
|
|
765
1018
|
await cleanupWebhook?.();
|
|
766
1019
|
stop();
|
|
767
1020
|
runtime.log?.(`[${account.accountId}] Zalo provider stopped mode=${mode}`);
|
|
@@ -771,4 +1024,5 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
|
|
|
771
1024
|
export const __testing = {
|
|
772
1025
|
evaluateZaloGroupAccess,
|
|
773
1026
|
resolveZaloRuntimeGroupPolicy,
|
|
1027
|
+
clearHostedMediaRouteRefsForTest: () => hostedMediaRouteRefs.clear(),
|
|
774
1028
|
};
|