@openclaw/zalo 2026.2.21 → 2026.2.23
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 +6 -0
- package/package.json +1 -1
- package/src/actions.ts +2 -13
- package/src/channel.ts +2 -11
- package/src/monitor.ts +23 -38
- package/src/monitor.webhook.test.ts +118 -226
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/actions.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ChannelMessageActionName,
|
|
4
4
|
OpenClawConfig,
|
|
5
5
|
} from "openclaw/plugin-sdk";
|
|
6
|
-
import { jsonResult, readStringParam } from "openclaw/plugin-sdk";
|
|
6
|
+
import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk";
|
|
7
7
|
import { listEnabledZaloAccounts } from "./accounts.js";
|
|
8
8
|
import { sendMessageZalo } from "./send.js";
|
|
9
9
|
|
|
@@ -25,18 +25,7 @@ export const zaloMessageActions: ChannelMessageActionAdapter = {
|
|
|
25
25
|
return Array.from(actions);
|
|
26
26
|
},
|
|
27
27
|
supportsButtons: () => false,
|
|
28
|
-
extractToolSend: ({ args }) =>
|
|
29
|
-
const action = typeof args.action === "string" ? args.action.trim() : "";
|
|
30
|
-
if (action !== "sendMessage") {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
const to = typeof args.to === "string" ? args.to : undefined;
|
|
34
|
-
if (!to) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
|
38
|
-
return { to, accountId };
|
|
39
|
-
},
|
|
28
|
+
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
40
29
|
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
41
30
|
if (action === "send") {
|
|
42
31
|
const to = readStringParam(params, "to", { required: true });
|
package/src/channel.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
applyAccountNameToChannelSection,
|
|
9
9
|
buildChannelConfigSchema,
|
|
10
|
+
buildTokenChannelStatusSummary,
|
|
10
11
|
DEFAULT_ACCOUNT_ID,
|
|
11
12
|
deleteAccountFromConfigSection,
|
|
12
13
|
chunkTextForOutbound,
|
|
@@ -309,17 +310,7 @@ export const zaloPlugin: ChannelPlugin<ResolvedZaloAccount> = {
|
|
|
309
310
|
lastError: null,
|
|
310
311
|
},
|
|
311
312
|
collectStatusIssues: collectZaloStatusIssues,
|
|
312
|
-
buildChannelSummary: ({ snapshot }) => (
|
|
313
|
-
configured: snapshot.configured ?? false,
|
|
314
|
-
tokenSource: snapshot.tokenSource ?? "none",
|
|
315
|
-
running: snapshot.running ?? false,
|
|
316
|
-
mode: snapshot.mode ?? null,
|
|
317
|
-
lastStartAt: snapshot.lastStartAt ?? null,
|
|
318
|
-
lastStopAt: snapshot.lastStopAt ?? null,
|
|
319
|
-
lastError: snapshot.lastError ?? null,
|
|
320
|
-
probe: snapshot.probe,
|
|
321
|
-
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
322
|
-
}),
|
|
313
|
+
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
|
|
323
314
|
probeAccount: async ({ account, timeoutMs }) =>
|
|
324
315
|
probeZalo(account.token, timeoutMs, resolveZaloProxyFetch(account.config.proxy)),
|
|
325
316
|
buildAccountSnapshot: ({ account, runtime }) => {
|
package/src/monitor.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
-
import type { OpenClawConfig,
|
|
3
|
+
import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk";
|
|
4
4
|
import {
|
|
5
|
+
createDedupeCache,
|
|
5
6
|
createReplyPrefixOptions,
|
|
6
7
|
readJsonBodyWithLimit,
|
|
7
8
|
registerWebhookTarget,
|
|
8
9
|
rejectNonPostWebhookRequest,
|
|
9
10
|
resolveSingleWebhookTarget,
|
|
10
11
|
resolveSenderCommandAuthorization,
|
|
12
|
+
resolveOutboundMediaUrls,
|
|
13
|
+
sendMediaWithLeadingCaption,
|
|
11
14
|
resolveWebhookPath,
|
|
12
15
|
resolveWebhookTargets,
|
|
13
16
|
requestBodyErrorToText,
|
|
@@ -92,7 +95,10 @@ type WebhookTarget = {
|
|
|
92
95
|
|
|
93
96
|
const webhookTargets = new Map<string, WebhookTarget[]>();
|
|
94
97
|
const webhookRateLimits = new Map<string, WebhookRateLimitState>();
|
|
95
|
-
const recentWebhookEvents =
|
|
98
|
+
const recentWebhookEvents = createDedupeCache({
|
|
99
|
+
ttlMs: ZALO_WEBHOOK_REPLAY_WINDOW_MS,
|
|
100
|
+
maxSize: 5000,
|
|
101
|
+
});
|
|
96
102
|
const webhookStatusCounters = new Map<string, number>();
|
|
97
103
|
|
|
98
104
|
function isJsonContentType(value: string | string[] | undefined): boolean {
|
|
@@ -141,22 +147,7 @@ function isReplayEvent(update: ZaloUpdate, nowMs: number): boolean {
|
|
|
141
147
|
return false;
|
|
142
148
|
}
|
|
143
149
|
const key = `${update.event_name}:${messageId}`;
|
|
144
|
-
|
|
145
|
-
recentWebhookEvents.set(key, nowMs);
|
|
146
|
-
|
|
147
|
-
if (seenAt && nowMs - seenAt < ZALO_WEBHOOK_REPLAY_WINDOW_MS) {
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (recentWebhookEvents.size > 5000) {
|
|
152
|
-
for (const [eventKey, timestamp] of recentWebhookEvents) {
|
|
153
|
-
if (nowMs - timestamp >= ZALO_WEBHOOK_REPLAY_WINDOW_MS) {
|
|
154
|
-
recentWebhookEvents.delete(eventKey);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return false;
|
|
150
|
+
return recentWebhookEvents.check(key, nowMs);
|
|
160
151
|
}
|
|
161
152
|
|
|
162
153
|
function recordWebhookStatus(
|
|
@@ -447,7 +438,7 @@ async function handleImageMessage(
|
|
|
447
438
|
if (photo) {
|
|
448
439
|
try {
|
|
449
440
|
const maxBytes = mediaMaxMb * 1024 * 1024;
|
|
450
|
-
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo });
|
|
441
|
+
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo, maxBytes });
|
|
451
442
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
452
443
|
fetched.buffer,
|
|
453
444
|
fetched.contentType,
|
|
@@ -692,7 +683,7 @@ async function processMessageWithPipeline(params: {
|
|
|
692
683
|
}
|
|
693
684
|
|
|
694
685
|
async function deliverZaloReply(params: {
|
|
695
|
-
payload:
|
|
686
|
+
payload: OutboundReplyPayload;
|
|
696
687
|
token: string;
|
|
697
688
|
chatId: string;
|
|
698
689
|
runtime: ZaloRuntimeEnv;
|
|
@@ -707,24 +698,18 @@ async function deliverZaloReply(params: {
|
|
|
707
698
|
const tableMode = params.tableMode ?? "code";
|
|
708
699
|
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
|
709
700
|
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
:
|
|
713
|
-
|
|
714
|
-
:
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
await sendPhoto(token, { chat_id: chatId, photo: mediaUrl, caption }, fetcher);
|
|
723
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
724
|
-
} catch (err) {
|
|
725
|
-
runtime.error?.(`Zalo photo send failed: ${String(err)}`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
701
|
+
const sentMedia = await sendMediaWithLeadingCaption({
|
|
702
|
+
mediaUrls: resolveOutboundMediaUrls(payload),
|
|
703
|
+
caption: text,
|
|
704
|
+
send: async ({ mediaUrl, caption }) => {
|
|
705
|
+
await sendPhoto(token, { chat_id: chatId, photo: mediaUrl, caption }, fetcher);
|
|
706
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
707
|
+
},
|
|
708
|
+
onError: (error) => {
|
|
709
|
+
runtime.error?.(`Zalo photo send failed: ${String(error)}`);
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
if (sentMedia) {
|
|
728
713
|
return;
|
|
729
714
|
}
|
|
730
715
|
|
|
@@ -21,113 +21,84 @@ async function withServer(handler: RequestListener, fn: (baseUrl: string) => Pro
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const DEFAULT_ACCOUNT: ResolvedZaloAccount = {
|
|
25
|
+
accountId: "default",
|
|
26
|
+
enabled: true,
|
|
27
|
+
token: "tok",
|
|
28
|
+
tokenSource: "config",
|
|
29
|
+
config: {},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const webhookRequestHandler: RequestListener = async (req, res) => {
|
|
33
|
+
const handled = await handleZaloWebhookRequest(req, res);
|
|
34
|
+
if (!handled) {
|
|
35
|
+
res.statusCode = 404;
|
|
36
|
+
res.end("not found");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function registerTarget(params: {
|
|
41
|
+
path: string;
|
|
42
|
+
secret?: string;
|
|
43
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
44
|
+
}): () => void {
|
|
45
|
+
return registerZaloWebhookTarget({
|
|
46
|
+
token: "tok",
|
|
47
|
+
account: DEFAULT_ACCOUNT,
|
|
48
|
+
config: {} as OpenClawConfig,
|
|
49
|
+
runtime: {},
|
|
50
|
+
core: {} as PluginRuntime,
|
|
51
|
+
secret: params.secret ?? "secret",
|
|
52
|
+
path: params.path,
|
|
53
|
+
mediaMaxMb: 5,
|
|
54
|
+
statusSink: params.statusSink,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
describe("handleZaloWebhookRequest", () => {
|
|
25
59
|
it("returns 400 for non-object payloads", async () => {
|
|
26
|
-
const
|
|
27
|
-
const account: ResolvedZaloAccount = {
|
|
28
|
-
accountId: "default",
|
|
29
|
-
enabled: true,
|
|
30
|
-
token: "tok",
|
|
31
|
-
tokenSource: "config",
|
|
32
|
-
config: {},
|
|
33
|
-
};
|
|
34
|
-
const unregister = registerZaloWebhookTarget({
|
|
35
|
-
token: "tok",
|
|
36
|
-
account,
|
|
37
|
-
config: {} as OpenClawConfig,
|
|
38
|
-
runtime: {},
|
|
39
|
-
core,
|
|
40
|
-
secret: "secret",
|
|
41
|
-
path: "/hook",
|
|
42
|
-
mediaMaxMb: 5,
|
|
43
|
-
});
|
|
60
|
+
const unregister = registerTarget({ path: "/hook" });
|
|
44
61
|
|
|
45
62
|
try {
|
|
46
|
-
await withServer(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"content-type": "application/json",
|
|
60
|
-
},
|
|
61
|
-
body: "null",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(response.status).toBe(400);
|
|
65
|
-
expect(await response.text()).toBe("Bad Request");
|
|
66
|
-
},
|
|
67
|
-
);
|
|
63
|
+
await withServer(webhookRequestHandler, async (baseUrl) => {
|
|
64
|
+
const response = await fetch(`${baseUrl}/hook`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"x-bot-api-secret-token": "secret",
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
},
|
|
70
|
+
body: "null",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(response.status).toBe(400);
|
|
74
|
+
expect(await response.text()).toBe("Bad Request");
|
|
75
|
+
});
|
|
68
76
|
} finally {
|
|
69
77
|
unregister();
|
|
70
78
|
}
|
|
71
79
|
});
|
|
72
80
|
|
|
73
81
|
it("rejects ambiguous routing when multiple targets match the same secret", async () => {
|
|
74
|
-
const core = {} as PluginRuntime;
|
|
75
|
-
const account: ResolvedZaloAccount = {
|
|
76
|
-
accountId: "default",
|
|
77
|
-
enabled: true,
|
|
78
|
-
token: "tok",
|
|
79
|
-
tokenSource: "config",
|
|
80
|
-
config: {},
|
|
81
|
-
};
|
|
82
82
|
const sinkA = vi.fn();
|
|
83
83
|
const sinkB = vi.fn();
|
|
84
|
-
const unregisterA =
|
|
85
|
-
|
|
86
|
-
account,
|
|
87
|
-
config: {} as OpenClawConfig,
|
|
88
|
-
runtime: {},
|
|
89
|
-
core,
|
|
90
|
-
secret: "secret",
|
|
91
|
-
path: "/hook",
|
|
92
|
-
mediaMaxMb: 5,
|
|
93
|
-
statusSink: sinkA,
|
|
94
|
-
});
|
|
95
|
-
const unregisterB = registerZaloWebhookTarget({
|
|
96
|
-
token: "tok",
|
|
97
|
-
account,
|
|
98
|
-
config: {} as OpenClawConfig,
|
|
99
|
-
runtime: {},
|
|
100
|
-
core,
|
|
101
|
-
secret: "secret",
|
|
102
|
-
path: "/hook",
|
|
103
|
-
mediaMaxMb: 5,
|
|
104
|
-
statusSink: sinkB,
|
|
105
|
-
});
|
|
84
|
+
const unregisterA = registerTarget({ path: "/hook", statusSink: sinkA });
|
|
85
|
+
const unregisterB = registerTarget({ path: "/hook", statusSink: sinkB });
|
|
106
86
|
|
|
107
87
|
try {
|
|
108
|
-
await withServer(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
body: "{}",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(response.status).toBe(401);
|
|
127
|
-
expect(sinkA).not.toHaveBeenCalled();
|
|
128
|
-
expect(sinkB).not.toHaveBeenCalled();
|
|
129
|
-
},
|
|
130
|
-
);
|
|
88
|
+
await withServer(webhookRequestHandler, async (baseUrl) => {
|
|
89
|
+
const response = await fetch(`${baseUrl}/hook`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"x-bot-api-secret-token": "secret",
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
body: "{}",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(response.status).toBe(401);
|
|
99
|
+
expect(sinkA).not.toHaveBeenCalled();
|
|
100
|
+
expect(sinkB).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
131
102
|
} finally {
|
|
132
103
|
unregisterA();
|
|
133
104
|
unregisterB();
|
|
@@ -135,73 +106,29 @@ describe("handleZaloWebhookRequest", () => {
|
|
|
135
106
|
});
|
|
136
107
|
|
|
137
108
|
it("returns 415 for non-json content-type", async () => {
|
|
138
|
-
const
|
|
139
|
-
const account: ResolvedZaloAccount = {
|
|
140
|
-
accountId: "default",
|
|
141
|
-
enabled: true,
|
|
142
|
-
token: "tok",
|
|
143
|
-
tokenSource: "config",
|
|
144
|
-
config: {},
|
|
145
|
-
};
|
|
146
|
-
const unregister = registerZaloWebhookTarget({
|
|
147
|
-
token: "tok",
|
|
148
|
-
account,
|
|
149
|
-
config: {} as OpenClawConfig,
|
|
150
|
-
runtime: {},
|
|
151
|
-
core,
|
|
152
|
-
secret: "secret",
|
|
153
|
-
path: "/hook-content-type",
|
|
154
|
-
mediaMaxMb: 5,
|
|
155
|
-
});
|
|
109
|
+
const unregister = registerTarget({ path: "/hook-content-type" });
|
|
156
110
|
|
|
157
111
|
try {
|
|
158
|
-
await withServer(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"x-bot-api-secret-token": "secret",
|
|
171
|
-
"content-type": "text/plain",
|
|
172
|
-
},
|
|
173
|
-
body: "{}",
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(response.status).toBe(415);
|
|
177
|
-
},
|
|
178
|
-
);
|
|
112
|
+
await withServer(webhookRequestHandler, async (baseUrl) => {
|
|
113
|
+
const response = await fetch(`${baseUrl}/hook-content-type`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"x-bot-api-secret-token": "secret",
|
|
117
|
+
"content-type": "text/plain",
|
|
118
|
+
},
|
|
119
|
+
body: "{}",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(415);
|
|
123
|
+
});
|
|
179
124
|
} finally {
|
|
180
125
|
unregister();
|
|
181
126
|
}
|
|
182
127
|
});
|
|
183
128
|
|
|
184
129
|
it("deduplicates webhook replay by event_name + message_id", async () => {
|
|
185
|
-
const core = {} as PluginRuntime;
|
|
186
|
-
const account: ResolvedZaloAccount = {
|
|
187
|
-
accountId: "default",
|
|
188
|
-
enabled: true,
|
|
189
|
-
token: "tok",
|
|
190
|
-
tokenSource: "config",
|
|
191
|
-
config: {},
|
|
192
|
-
};
|
|
193
130
|
const sink = vi.fn();
|
|
194
|
-
const unregister =
|
|
195
|
-
token: "tok",
|
|
196
|
-
account,
|
|
197
|
-
config: {} as OpenClawConfig,
|
|
198
|
-
runtime: {},
|
|
199
|
-
core,
|
|
200
|
-
secret: "secret",
|
|
201
|
-
path: "/hook-replay",
|
|
202
|
-
mediaMaxMb: 5,
|
|
203
|
-
statusSink: sink,
|
|
204
|
-
});
|
|
131
|
+
const unregister = registerTarget({ path: "/hook-replay", statusSink: sink });
|
|
205
132
|
|
|
206
133
|
const payload = {
|
|
207
134
|
event_name: "message.text.received",
|
|
@@ -215,91 +142,56 @@ describe("handleZaloWebhookRequest", () => {
|
|
|
215
142
|
};
|
|
216
143
|
|
|
217
144
|
try {
|
|
218
|
-
await withServer(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
},
|
|
241
|
-
body: JSON.stringify(payload),
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
expect(first.status).toBe(200);
|
|
245
|
-
expect(second.status).toBe(200);
|
|
246
|
-
expect(sink).toHaveBeenCalledTimes(1);
|
|
247
|
-
},
|
|
248
|
-
);
|
|
145
|
+
await withServer(webhookRequestHandler, async (baseUrl) => {
|
|
146
|
+
const first = await fetch(`${baseUrl}/hook-replay`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"x-bot-api-secret-token": "secret",
|
|
150
|
+
"content-type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify(payload),
|
|
153
|
+
});
|
|
154
|
+
const second = await fetch(`${baseUrl}/hook-replay`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
"x-bot-api-secret-token": "secret",
|
|
158
|
+
"content-type": "application/json",
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify(payload),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(first.status).toBe(200);
|
|
164
|
+
expect(second.status).toBe(200);
|
|
165
|
+
expect(sink).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
249
167
|
} finally {
|
|
250
168
|
unregister();
|
|
251
169
|
}
|
|
252
170
|
});
|
|
253
171
|
|
|
254
172
|
it("returns 429 when per-path request rate exceeds threshold", async () => {
|
|
255
|
-
const
|
|
256
|
-
const account: ResolvedZaloAccount = {
|
|
257
|
-
accountId: "default",
|
|
258
|
-
enabled: true,
|
|
259
|
-
token: "tok",
|
|
260
|
-
tokenSource: "config",
|
|
261
|
-
config: {},
|
|
262
|
-
};
|
|
263
|
-
const unregister = registerZaloWebhookTarget({
|
|
264
|
-
token: "tok",
|
|
265
|
-
account,
|
|
266
|
-
config: {} as OpenClawConfig,
|
|
267
|
-
runtime: {},
|
|
268
|
-
core,
|
|
269
|
-
secret: "secret",
|
|
270
|
-
path: "/hook-rate",
|
|
271
|
-
mediaMaxMb: 5,
|
|
272
|
-
});
|
|
173
|
+
const unregister = registerTarget({ path: "/hook-rate" });
|
|
273
174
|
|
|
274
175
|
try {
|
|
275
|
-
await withServer(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
"x-bot-api-secret-token": "secret",
|
|
290
|
-
"content-type": "application/json",
|
|
291
|
-
},
|
|
292
|
-
body: "{}",
|
|
293
|
-
});
|
|
294
|
-
if (response.status === 429) {
|
|
295
|
-
saw429 = true;
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
176
|
+
await withServer(webhookRequestHandler, async (baseUrl) => {
|
|
177
|
+
let saw429 = false;
|
|
178
|
+
for (let i = 0; i < 130; i += 1) {
|
|
179
|
+
const response = await fetch(`${baseUrl}/hook-rate`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
"x-bot-api-secret-token": "secret",
|
|
183
|
+
"content-type": "application/json",
|
|
184
|
+
},
|
|
185
|
+
body: "{}",
|
|
186
|
+
});
|
|
187
|
+
if (response.status === 429) {
|
|
188
|
+
saw429 = true;
|
|
189
|
+
break;
|
|
298
190
|
}
|
|
191
|
+
}
|
|
299
192
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
193
|
+
expect(saw429).toBe(true);
|
|
194
|
+
});
|
|
303
195
|
} finally {
|
|
304
196
|
unregister();
|
|
305
197
|
}
|