@openclaw/zalo 2026.2.19 → 2026.2.22
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 +1 -127
- package/package.json +1 -1
- package/src/monitor.ts +14 -22
- package/src/monitor.webhook.test.ts +118 -226
package/CHANGELOG.md
CHANGED
|
@@ -1,132 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2026.2.
|
|
4
|
-
|
|
5
|
-
### Changes
|
|
6
|
-
|
|
7
|
-
- Version alignment with core OpenClaw release numbers.
|
|
8
|
-
|
|
9
|
-
## 2026.2.16
|
|
10
|
-
|
|
11
|
-
### Changes
|
|
12
|
-
|
|
13
|
-
- Version alignment with core OpenClaw release numbers.
|
|
14
|
-
|
|
15
|
-
## 2026.2.15
|
|
16
|
-
|
|
17
|
-
### Changes
|
|
18
|
-
|
|
19
|
-
- Version alignment with core OpenClaw release numbers.
|
|
20
|
-
|
|
21
|
-
## 2026.2.14
|
|
22
|
-
|
|
23
|
-
### Changes
|
|
24
|
-
|
|
25
|
-
- Version alignment with core OpenClaw release numbers.
|
|
26
|
-
|
|
27
|
-
## 2026.2.13
|
|
28
|
-
|
|
29
|
-
### Changes
|
|
30
|
-
|
|
31
|
-
- Version alignment with core OpenClaw release numbers.
|
|
32
|
-
|
|
33
|
-
## 2026.2.6-3
|
|
34
|
-
|
|
35
|
-
### Changes
|
|
36
|
-
|
|
37
|
-
- Version alignment with core OpenClaw release numbers.
|
|
38
|
-
|
|
39
|
-
## 2026.2.6-2
|
|
40
|
-
|
|
41
|
-
### Changes
|
|
42
|
-
|
|
43
|
-
- Version alignment with core OpenClaw release numbers.
|
|
44
|
-
|
|
45
|
-
## 2026.2.6
|
|
46
|
-
|
|
47
|
-
### Changes
|
|
48
|
-
|
|
49
|
-
- Version alignment with core OpenClaw release numbers.
|
|
50
|
-
|
|
51
|
-
## 2026.2.4
|
|
52
|
-
|
|
53
|
-
### Changes
|
|
54
|
-
|
|
55
|
-
- Version alignment with core OpenClaw release numbers.
|
|
56
|
-
|
|
57
|
-
## 2026.2.2
|
|
58
|
-
|
|
59
|
-
### Changes
|
|
60
|
-
|
|
61
|
-
- Version alignment with core OpenClaw release numbers.
|
|
62
|
-
|
|
63
|
-
## 2026.1.31
|
|
64
|
-
|
|
65
|
-
### Changes
|
|
66
|
-
|
|
67
|
-
- Version alignment with core OpenClaw release numbers.
|
|
68
|
-
|
|
69
|
-
## 2026.1.30
|
|
70
|
-
|
|
71
|
-
### Changes
|
|
72
|
-
|
|
73
|
-
- Version alignment with core OpenClaw release numbers.
|
|
74
|
-
|
|
75
|
-
## 2026.1.29
|
|
76
|
-
|
|
77
|
-
### Changes
|
|
78
|
-
|
|
79
|
-
- Version alignment with core OpenClaw release numbers.
|
|
80
|
-
|
|
81
|
-
## 2026.1.23
|
|
82
|
-
|
|
83
|
-
### Changes
|
|
84
|
-
|
|
85
|
-
- Version alignment with core OpenClaw release numbers.
|
|
86
|
-
|
|
87
|
-
## 2026.1.22
|
|
88
|
-
|
|
89
|
-
### Changes
|
|
90
|
-
|
|
91
|
-
- Version alignment with core OpenClaw release numbers.
|
|
92
|
-
|
|
93
|
-
## 2026.1.21
|
|
94
|
-
|
|
95
|
-
### Changes
|
|
96
|
-
|
|
97
|
-
- Version alignment with core OpenClaw release numbers.
|
|
98
|
-
|
|
99
|
-
## 2026.1.20
|
|
100
|
-
|
|
101
|
-
### Changes
|
|
102
|
-
|
|
103
|
-
- Version alignment with core OpenClaw release numbers.
|
|
104
|
-
|
|
105
|
-
## 2026.1.17-1
|
|
106
|
-
|
|
107
|
-
### Changes
|
|
108
|
-
|
|
109
|
-
- Version alignment with core OpenClaw release numbers.
|
|
110
|
-
|
|
111
|
-
## 2026.1.17
|
|
112
|
-
|
|
113
|
-
### Changes
|
|
114
|
-
|
|
115
|
-
- Version alignment with core OpenClaw release numbers.
|
|
116
|
-
|
|
117
|
-
## 2026.1.16
|
|
118
|
-
|
|
119
|
-
### Changes
|
|
120
|
-
|
|
121
|
-
- Version alignment with core OpenClaw release numbers.
|
|
122
|
-
|
|
123
|
-
## 2026.1.15
|
|
124
|
-
|
|
125
|
-
### Changes
|
|
126
|
-
|
|
127
|
-
- Version alignment with core OpenClaw release numbers.
|
|
128
|
-
|
|
129
|
-
## 2026.1.14
|
|
3
|
+
## 2026.2.22
|
|
130
4
|
|
|
131
5
|
### Changes
|
|
132
6
|
|
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
3
|
import type { OpenClawConfig, MarkdownTableMode } from "openclaw/plugin-sdk";
|
|
4
4
|
import {
|
|
5
|
+
createDedupeCache,
|
|
5
6
|
createReplyPrefixOptions,
|
|
6
7
|
readJsonBodyWithLimit,
|
|
7
8
|
registerWebhookTarget,
|
|
8
9
|
rejectNonPostWebhookRequest,
|
|
10
|
+
resolveSingleWebhookTarget,
|
|
9
11
|
resolveSenderCommandAuthorization,
|
|
10
12
|
resolveWebhookPath,
|
|
11
13
|
resolveWebhookTargets,
|
|
@@ -91,7 +93,10 @@ type WebhookTarget = {
|
|
|
91
93
|
|
|
92
94
|
const webhookTargets = new Map<string, WebhookTarget[]>();
|
|
93
95
|
const webhookRateLimits = new Map<string, WebhookRateLimitState>();
|
|
94
|
-
const recentWebhookEvents =
|
|
96
|
+
const recentWebhookEvents = createDedupeCache({
|
|
97
|
+
ttlMs: ZALO_WEBHOOK_REPLAY_WINDOW_MS,
|
|
98
|
+
maxSize: 5000,
|
|
99
|
+
});
|
|
95
100
|
const webhookStatusCounters = new Map<string, number>();
|
|
96
101
|
|
|
97
102
|
function isJsonContentType(value: string | string[] | undefined): boolean {
|
|
@@ -140,22 +145,7 @@ function isReplayEvent(update: ZaloUpdate, nowMs: number): boolean {
|
|
|
140
145
|
return false;
|
|
141
146
|
}
|
|
142
147
|
const key = `${update.event_name}:${messageId}`;
|
|
143
|
-
|
|
144
|
-
recentWebhookEvents.set(key, nowMs);
|
|
145
|
-
|
|
146
|
-
if (seenAt && nowMs - seenAt < ZALO_WEBHOOK_REPLAY_WINDOW_MS) {
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (recentWebhookEvents.size > 5000) {
|
|
151
|
-
for (const [eventKey, timestamp] of recentWebhookEvents) {
|
|
152
|
-
if (nowMs - timestamp >= ZALO_WEBHOOK_REPLAY_WINDOW_MS) {
|
|
153
|
-
recentWebhookEvents.delete(eventKey);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return false;
|
|
148
|
+
return recentWebhookEvents.check(key, nowMs);
|
|
159
149
|
}
|
|
160
150
|
|
|
161
151
|
function recordWebhookStatus(
|
|
@@ -195,20 +185,22 @@ export async function handleZaloWebhookRequest(
|
|
|
195
185
|
}
|
|
196
186
|
|
|
197
187
|
const headerToken = String(req.headers["x-bot-api-secret-token"] ?? "");
|
|
198
|
-
const
|
|
199
|
-
|
|
188
|
+
const matchedTarget = resolveSingleWebhookTarget(targets, (entry) =>
|
|
189
|
+
timingSafeEquals(entry.secret, headerToken),
|
|
190
|
+
);
|
|
191
|
+
if (matchedTarget.kind === "none") {
|
|
200
192
|
res.statusCode = 401;
|
|
201
193
|
res.end("unauthorized");
|
|
202
194
|
recordWebhookStatus(targets[0]?.runtime, req.url ?? "<unknown>", res.statusCode);
|
|
203
195
|
return true;
|
|
204
196
|
}
|
|
205
|
-
if (
|
|
197
|
+
if (matchedTarget.kind === "ambiguous") {
|
|
206
198
|
res.statusCode = 401;
|
|
207
199
|
res.end("ambiguous webhook target");
|
|
208
200
|
recordWebhookStatus(targets[0]?.runtime, req.url ?? "<unknown>", res.statusCode);
|
|
209
201
|
return true;
|
|
210
202
|
}
|
|
211
|
-
const target =
|
|
203
|
+
const target = matchedTarget.target;
|
|
212
204
|
const path = req.url ?? "<unknown>";
|
|
213
205
|
const rateLimitKey = `${path}:${req.socket.remoteAddress ?? "unknown"}`;
|
|
214
206
|
const nowMs = Date.now();
|
|
@@ -444,7 +436,7 @@ async function handleImageMessage(
|
|
|
444
436
|
if (photo) {
|
|
445
437
|
try {
|
|
446
438
|
const maxBytes = mediaMaxMb * 1024 * 1024;
|
|
447
|
-
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo });
|
|
439
|
+
const fetched = await core.channel.media.fetchRemoteMedia({ url: photo, maxBytes });
|
|
448
440
|
const saved = await core.channel.media.saveMediaBuffer(
|
|
449
441
|
fetched.buffer,
|
|
450
442
|
fetched.contentType,
|
|
@@ -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
|
}
|