@openclaw/feishu 2026.2.25 → 2026.3.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.
- package/index.ts +2 -0
- package/package.json +2 -1
- package/skills/feishu-doc/SKILL.md +109 -3
- package/src/accounts.test.ts +161 -0
- package/src/accounts.ts +76 -8
- package/src/async.ts +62 -0
- package/src/bitable.ts +189 -215
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +56 -1
- package/src/bot.test.ts +1271 -56
- package/src/bot.ts +499 -215
- package/src/card-action.ts +79 -0
- package/src/channel.ts +26 -4
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +121 -0
- package/src/client.ts +13 -0
- package/src/config-schema.test.ts +101 -1
- package/src/config-schema.ts +66 -11
- package/src/dedup.ts +47 -1
- package/src/doc-schema.ts +135 -0
- package/src/docx-batch-insert.ts +190 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +331 -9
- package/src/docx.ts +996 -72
- package/src/drive.ts +38 -33
- package/src/media.test.ts +227 -7
- package/src/media.ts +52 -11
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +534 -0
- package/src/monitor.reaction.test.ts +578 -0
- package/src/monitor.startup.test.ts +203 -0
- package/src/monitor.startup.ts +51 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +152 -0
- package/src/monitor.test-mocks.ts +12 -0
- package/src/monitor.transport.ts +163 -0
- package/src/monitor.ts +44 -346
- package/src/monitor.webhook-security.test.ts +53 -10
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.ts +144 -52
- package/src/outbound.test.ts +181 -0
- package/src/outbound.ts +94 -7
- package/src/perm.ts +37 -30
- package/src/policy.test.ts +56 -1
- package/src/policy.ts +5 -1
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +271 -0
- package/src/probe.ts +131 -19
- package/src/reply-dispatcher.test.ts +300 -0
- package/src/reply-dispatcher.ts +159 -46
- package/src/secret-input.ts +19 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +6 -2
- package/src/send.reply-fallback.test.ts +105 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +143 -18
- package/src/streaming-card.ts +131 -43
- package/src/targets.test.ts +55 -1
- package/src/targets.ts +32 -7
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +10 -1
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +140 -10
- package/src/wiki.ts +55 -50
package/src/monitor.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
} from "
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
monitorSingleAccount,
|
|
5
|
+
resolveReactionSyntheticEvent,
|
|
6
|
+
type FeishuReactionCreatedEvent,
|
|
7
|
+
} from "./monitor.account.js";
|
|
8
|
+
import { fetchBotOpenIdForMonitor } from "./monitor.startup.js";
|
|
9
|
+
import {
|
|
10
|
+
clearFeishuWebhookRateLimitStateForTest,
|
|
11
|
+
getFeishuWebhookRateLimitStateSizeForTest,
|
|
12
|
+
isWebhookRateLimitedForTest,
|
|
13
|
+
stopFeishuMonitorState,
|
|
14
|
+
} from "./monitor.state.js";
|
|
14
15
|
|
|
15
16
|
export type MonitorFeishuOpts = {
|
|
16
17
|
config?: ClawdbotConfig;
|
|
@@ -19,316 +20,14 @@ export type MonitorFeishuOpts = {
|
|
|
19
20
|
accountId?: string;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
|
28
|
-
const FEISHU_WEBHOOK_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
29
|
-
const FEISHU_WEBHOOK_RATE_LIMIT_MAX_REQUESTS = 120;
|
|
30
|
-
const FEISHU_WEBHOOK_COUNTER_LOG_EVERY = 25;
|
|
31
|
-
const feishuWebhookRateLimits = new Map<string, { count: number; windowStartMs: number }>();
|
|
32
|
-
const feishuWebhookStatusCounters = new Map<string, number>();
|
|
33
|
-
|
|
34
|
-
function isJsonContentType(value: string | string[] | undefined): boolean {
|
|
35
|
-
const first = Array.isArray(value) ? value[0] : value;
|
|
36
|
-
if (!first) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
const mediaType = first.split(";", 1)[0]?.trim().toLowerCase();
|
|
40
|
-
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isWebhookRateLimited(key: string, nowMs: number): boolean {
|
|
44
|
-
const state = feishuWebhookRateLimits.get(key);
|
|
45
|
-
if (!state || nowMs - state.windowStartMs >= FEISHU_WEBHOOK_RATE_LIMIT_WINDOW_MS) {
|
|
46
|
-
feishuWebhookRateLimits.set(key, { count: 1, windowStartMs: nowMs });
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
state.count += 1;
|
|
51
|
-
if (state.count > FEISHU_WEBHOOK_RATE_LIMIT_MAX_REQUESTS) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function recordWebhookStatus(
|
|
58
|
-
runtime: RuntimeEnv | undefined,
|
|
59
|
-
accountId: string,
|
|
60
|
-
path: string,
|
|
61
|
-
statusCode: number,
|
|
62
|
-
): void {
|
|
63
|
-
if (![400, 401, 408, 413, 415, 429].includes(statusCode)) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const key = `${accountId}:${path}:${statusCode}`;
|
|
67
|
-
const next = (feishuWebhookStatusCounters.get(key) ?? 0) + 1;
|
|
68
|
-
feishuWebhookStatusCounters.set(key, next);
|
|
69
|
-
if (next === 1 || next % FEISHU_WEBHOOK_COUNTER_LOG_EVERY === 0) {
|
|
70
|
-
const log = runtime?.log ?? console.log;
|
|
71
|
-
log(`feishu[${accountId}]: webhook anomaly path=${path} status=${statusCode} count=${next}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function fetchBotOpenId(account: ResolvedFeishuAccount): Promise<string | undefined> {
|
|
76
|
-
try {
|
|
77
|
-
const result = await probeFeishu(account);
|
|
78
|
-
return result.ok ? result.botOpenId : undefined;
|
|
79
|
-
} catch {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Register common event handlers on an EventDispatcher.
|
|
86
|
-
* When fireAndForget is true (webhook mode), message handling is not awaited
|
|
87
|
-
* to avoid blocking the HTTP response (Lark requires <3s response).
|
|
88
|
-
*/
|
|
89
|
-
function registerEventHandlers(
|
|
90
|
-
eventDispatcher: Lark.EventDispatcher,
|
|
91
|
-
context: {
|
|
92
|
-
cfg: ClawdbotConfig;
|
|
93
|
-
accountId: string;
|
|
94
|
-
runtime?: RuntimeEnv;
|
|
95
|
-
chatHistories: Map<string, HistoryEntry[]>;
|
|
96
|
-
fireAndForget?: boolean;
|
|
97
|
-
},
|
|
98
|
-
) {
|
|
99
|
-
const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
|
|
100
|
-
const log = runtime?.log ?? console.log;
|
|
101
|
-
const error = runtime?.error ?? console.error;
|
|
102
|
-
|
|
103
|
-
eventDispatcher.register({
|
|
104
|
-
"im.message.receive_v1": async (data) => {
|
|
105
|
-
try {
|
|
106
|
-
const event = data as unknown as FeishuMessageEvent;
|
|
107
|
-
const promise = handleFeishuMessage({
|
|
108
|
-
cfg,
|
|
109
|
-
event,
|
|
110
|
-
botOpenId: botOpenIds.get(accountId),
|
|
111
|
-
runtime,
|
|
112
|
-
chatHistories,
|
|
113
|
-
accountId,
|
|
114
|
-
});
|
|
115
|
-
if (fireAndForget) {
|
|
116
|
-
promise.catch((err) => {
|
|
117
|
-
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
|
118
|
-
});
|
|
119
|
-
} else {
|
|
120
|
-
await promise;
|
|
121
|
-
}
|
|
122
|
-
} catch (err) {
|
|
123
|
-
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
"im.message.message_read_v1": async () => {
|
|
127
|
-
// Ignore read receipts
|
|
128
|
-
},
|
|
129
|
-
"im.chat.member.bot.added_v1": async (data) => {
|
|
130
|
-
try {
|
|
131
|
-
const event = data as unknown as FeishuBotAddedEvent;
|
|
132
|
-
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
"im.chat.member.bot.deleted_v1": async (data) => {
|
|
138
|
-
try {
|
|
139
|
-
const event = data as unknown as { chat_id: string };
|
|
140
|
-
log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
|
|
141
|
-
} catch (err) {
|
|
142
|
-
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
type MonitorAccountParams = {
|
|
149
|
-
cfg: ClawdbotConfig;
|
|
150
|
-
account: ResolvedFeishuAccount;
|
|
151
|
-
runtime?: RuntimeEnv;
|
|
152
|
-
abortSignal?: AbortSignal;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Monitor a single Feishu account.
|
|
157
|
-
*/
|
|
158
|
-
async function monitorSingleAccount(params: MonitorAccountParams): Promise<void> {
|
|
159
|
-
const { cfg, account, runtime, abortSignal } = params;
|
|
160
|
-
const { accountId } = account;
|
|
161
|
-
const log = runtime?.log ?? console.log;
|
|
162
|
-
|
|
163
|
-
// Fetch bot open_id
|
|
164
|
-
const botOpenId = await fetchBotOpenId(account);
|
|
165
|
-
botOpenIds.set(accountId, botOpenId ?? "");
|
|
166
|
-
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
167
|
-
|
|
168
|
-
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
169
|
-
if (connectionMode === "webhook" && !account.verificationToken?.trim()) {
|
|
170
|
-
throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
|
|
171
|
-
}
|
|
172
|
-
const eventDispatcher = createEventDispatcher(account);
|
|
173
|
-
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
174
|
-
|
|
175
|
-
registerEventHandlers(eventDispatcher, {
|
|
176
|
-
cfg,
|
|
177
|
-
accountId,
|
|
178
|
-
runtime,
|
|
179
|
-
chatHistories,
|
|
180
|
-
fireAndForget: connectionMode === "webhook",
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (connectionMode === "webhook") {
|
|
184
|
-
return monitorWebhook({ params, accountId, eventDispatcher });
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return monitorWebSocket({ params, accountId, eventDispatcher });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
type ConnectionParams = {
|
|
191
|
-
params: MonitorAccountParams;
|
|
192
|
-
accountId: string;
|
|
193
|
-
eventDispatcher: Lark.EventDispatcher;
|
|
23
|
+
export {
|
|
24
|
+
clearFeishuWebhookRateLimitStateForTest,
|
|
25
|
+
getFeishuWebhookRateLimitStateSizeForTest,
|
|
26
|
+
isWebhookRateLimitedForTest,
|
|
27
|
+
resolveReactionSyntheticEvent,
|
|
194
28
|
};
|
|
29
|
+
export type { FeishuReactionCreatedEvent };
|
|
195
30
|
|
|
196
|
-
async function monitorWebSocket({
|
|
197
|
-
params,
|
|
198
|
-
accountId,
|
|
199
|
-
eventDispatcher,
|
|
200
|
-
}: ConnectionParams): Promise<void> {
|
|
201
|
-
const { account, runtime, abortSignal } = params;
|
|
202
|
-
const log = runtime?.log ?? console.log;
|
|
203
|
-
const error = runtime?.error ?? console.error;
|
|
204
|
-
|
|
205
|
-
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
|
206
|
-
|
|
207
|
-
const wsClient = createFeishuWSClient(account);
|
|
208
|
-
wsClients.set(accountId, wsClient);
|
|
209
|
-
|
|
210
|
-
return new Promise((resolve, reject) => {
|
|
211
|
-
const cleanup = () => {
|
|
212
|
-
wsClients.delete(accountId);
|
|
213
|
-
botOpenIds.delete(accountId);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const handleAbort = () => {
|
|
217
|
-
log(`feishu[${accountId}]: abort signal received, stopping`);
|
|
218
|
-
cleanup();
|
|
219
|
-
resolve();
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
if (abortSignal?.aborted) {
|
|
223
|
-
cleanup();
|
|
224
|
-
resolve();
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
wsClient.start({ eventDispatcher });
|
|
232
|
-
log(`feishu[${accountId}]: WebSocket client started`);
|
|
233
|
-
} catch (err) {
|
|
234
|
-
cleanup();
|
|
235
|
-
abortSignal?.removeEventListener("abort", handleAbort);
|
|
236
|
-
reject(err);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function monitorWebhook({
|
|
242
|
-
params,
|
|
243
|
-
accountId,
|
|
244
|
-
eventDispatcher,
|
|
245
|
-
}: ConnectionParams): Promise<void> {
|
|
246
|
-
const { account, runtime, abortSignal } = params;
|
|
247
|
-
const log = runtime?.log ?? console.log;
|
|
248
|
-
const error = runtime?.error ?? console.error;
|
|
249
|
-
|
|
250
|
-
const port = account.config.webhookPort ?? 3000;
|
|
251
|
-
const path = account.config.webhookPath ?? "/feishu/events";
|
|
252
|
-
const host = account.config.webhookHost ?? "127.0.0.1";
|
|
253
|
-
|
|
254
|
-
log(`feishu[${accountId}]: starting Webhook server on ${host}:${port}, path ${path}...`);
|
|
255
|
-
|
|
256
|
-
const server = http.createServer();
|
|
257
|
-
const webhookHandler = Lark.adaptDefault(path, eventDispatcher, { autoChallenge: true });
|
|
258
|
-
server.on("request", (req, res) => {
|
|
259
|
-
res.on("finish", () => {
|
|
260
|
-
recordWebhookStatus(runtime, accountId, path, res.statusCode);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const rateLimitKey = `${accountId}:${path}:${req.socket.remoteAddress ?? "unknown"}`;
|
|
264
|
-
if (isWebhookRateLimited(rateLimitKey, Date.now())) {
|
|
265
|
-
res.statusCode = 429;
|
|
266
|
-
res.end("Too Many Requests");
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (req.method === "POST" && !isJsonContentType(req.headers["content-type"])) {
|
|
271
|
-
res.statusCode = 415;
|
|
272
|
-
res.end("Unsupported Media Type");
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const guard = installRequestBodyLimitGuard(req, res, {
|
|
277
|
-
maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
278
|
-
timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
|
|
279
|
-
responseFormat: "text",
|
|
280
|
-
});
|
|
281
|
-
if (guard.isTripped()) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
void Promise.resolve(webhookHandler(req, res))
|
|
285
|
-
.catch((err) => {
|
|
286
|
-
if (!guard.isTripped()) {
|
|
287
|
-
error(`feishu[${accountId}]: webhook handler error: ${String(err)}`);
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
.finally(() => {
|
|
291
|
-
guard.dispose();
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
httpServers.set(accountId, server);
|
|
295
|
-
|
|
296
|
-
return new Promise((resolve, reject) => {
|
|
297
|
-
const cleanup = () => {
|
|
298
|
-
server.close();
|
|
299
|
-
httpServers.delete(accountId);
|
|
300
|
-
botOpenIds.delete(accountId);
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const handleAbort = () => {
|
|
304
|
-
log(`feishu[${accountId}]: abort signal received, stopping Webhook server`);
|
|
305
|
-
cleanup();
|
|
306
|
-
resolve();
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
if (abortSignal?.aborted) {
|
|
310
|
-
cleanup();
|
|
311
|
-
resolve();
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
316
|
-
|
|
317
|
-
server.listen(port, host, () => {
|
|
318
|
-
log(`feishu[${accountId}]: Webhook server listening on ${host}:${port}`);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
server.on("error", (err) => {
|
|
322
|
-
error(`feishu[${accountId}]: Webhook server error: ${err}`);
|
|
323
|
-
abortSignal?.removeEventListener("abort", handleAbort);
|
|
324
|
-
reject(err);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Main entry: start monitoring for all enabled accounts.
|
|
331
|
-
*/
|
|
332
31
|
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
|
|
333
32
|
const cfg = opts.config;
|
|
334
33
|
if (!cfg) {
|
|
@@ -337,7 +36,6 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
337
36
|
|
|
338
37
|
const log = opts.runtime?.log ?? console.log;
|
|
339
38
|
|
|
340
|
-
// If accountId is specified, only monitor that account
|
|
341
39
|
if (opts.accountId) {
|
|
342
40
|
const account = resolveFeishuAccount({ cfg, accountId: opts.accountId });
|
|
343
41
|
if (!account.enabled || !account.configured) {
|
|
@@ -351,7 +49,6 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
351
49
|
});
|
|
352
50
|
}
|
|
353
51
|
|
|
354
|
-
// Otherwise, start all enabled accounts
|
|
355
52
|
const accounts = listEnabledFeishuAccounts(cfg);
|
|
356
53
|
if (accounts.length === 0) {
|
|
357
54
|
throw new Error("No enabled Feishu accounts configured");
|
|
@@ -361,37 +58,38 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
|
|
|
361
58
|
`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`,
|
|
362
59
|
);
|
|
363
60
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
61
|
+
const monitorPromises: Promise<void>[] = [];
|
|
62
|
+
for (const account of accounts) {
|
|
63
|
+
if (opts.abortSignal?.aborted) {
|
|
64
|
+
log("feishu: abort signal received during startup preflight; stopping startup");
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Probe sequentially so large multi-account startups do not burst Feishu's bot-info endpoint.
|
|
69
|
+
const botOpenId = await fetchBotOpenIdForMonitor(account, {
|
|
70
|
+
runtime: opts.runtime,
|
|
71
|
+
abortSignal: opts.abortSignal,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (opts.abortSignal?.aborted) {
|
|
75
|
+
log("feishu: abort signal received during startup preflight; stopping startup");
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
monitorPromises.push(
|
|
367
80
|
monitorSingleAccount({
|
|
368
81
|
cfg,
|
|
369
82
|
account,
|
|
370
83
|
runtime: opts.runtime,
|
|
371
84
|
abortSignal: opts.abortSignal,
|
|
85
|
+
botOpenIdSource: { kind: "prefetched", botOpenId },
|
|
372
86
|
}),
|
|
373
|
-
)
|
|
374
|
-
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await Promise.all(monitorPromises);
|
|
375
91
|
}
|
|
376
92
|
|
|
377
|
-
/**
|
|
378
|
-
* Stop monitoring for a specific account or all accounts.
|
|
379
|
-
*/
|
|
380
93
|
export function stopFeishuMonitor(accountId?: string): void {
|
|
381
|
-
|
|
382
|
-
wsClients.delete(accountId);
|
|
383
|
-
const server = httpServers.get(accountId);
|
|
384
|
-
if (server) {
|
|
385
|
-
server.close();
|
|
386
|
-
httpServers.delete(accountId);
|
|
387
|
-
}
|
|
388
|
-
botOpenIds.delete(accountId);
|
|
389
|
-
} else {
|
|
390
|
-
wsClients.clear();
|
|
391
|
-
for (const server of httpServers.values()) {
|
|
392
|
-
server.close();
|
|
393
|
-
}
|
|
394
|
-
httpServers.clear();
|
|
395
|
-
botOpenIds.clear();
|
|
396
|
-
}
|
|
94
|
+
stopFeishuMonitorState(accountId);
|
|
397
95
|
}
|
|
@@ -5,15 +5,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
|
|
6
6
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
7
7
|
|
|
8
|
-
vi.mock("@larksuiteoapi/node-sdk", () => ({
|
|
9
|
-
adaptDefault: vi.fn(
|
|
10
|
-
() => (_req: unknown, res: { statusCode?: number; end: (s: string) => void }) => {
|
|
11
|
-
res.statusCode = 200;
|
|
12
|
-
res.end("ok");
|
|
13
|
-
},
|
|
14
|
-
),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
8
|
vi.mock("./probe.js", () => ({
|
|
18
9
|
probeFeishu: probeFeishuMock,
|
|
19
10
|
}));
|
|
@@ -23,7 +14,39 @@ vi.mock("./client.js", () => ({
|
|
|
23
14
|
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
24
15
|
}));
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
vi.mock("./runtime.js", () => ({
|
|
18
|
+
getFeishuRuntime: () => ({
|
|
19
|
+
channel: {
|
|
20
|
+
debounce: {
|
|
21
|
+
resolveInboundDebounceMs: () => 0,
|
|
22
|
+
createInboundDebouncer: () => ({
|
|
23
|
+
enqueue: async () => {},
|
|
24
|
+
flushKey: async () => {},
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
text: {
|
|
28
|
+
hasControlCommand: () => false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("@larksuiteoapi/node-sdk", () => ({
|
|
35
|
+
adaptDefault: vi.fn(
|
|
36
|
+
() => (_req: unknown, res: { statusCode?: number; end: (s: string) => void }) => {
|
|
37
|
+
res.statusCode = 200;
|
|
38
|
+
res.end("ok");
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
clearFeishuWebhookRateLimitStateForTest,
|
|
45
|
+
getFeishuWebhookRateLimitStateSizeForTest,
|
|
46
|
+
isWebhookRateLimitedForTest,
|
|
47
|
+
monitorFeishuProvider,
|
|
48
|
+
stopFeishuMonitor,
|
|
49
|
+
} from "./monitor.js";
|
|
27
50
|
|
|
28
51
|
async function getFreePort(): Promise<number> {
|
|
29
52
|
const server = createServer();
|
|
@@ -114,6 +137,7 @@ async function withRunningWebhookMonitor(
|
|
|
114
137
|
}
|
|
115
138
|
|
|
116
139
|
afterEach(() => {
|
|
140
|
+
clearFeishuWebhookRateLimitStateForTest();
|
|
117
141
|
stopFeishuMonitor();
|
|
118
142
|
});
|
|
119
143
|
|
|
@@ -180,4 +204,23 @@ describe("Feishu webhook security hardening", () => {
|
|
|
180
204
|
},
|
|
181
205
|
);
|
|
182
206
|
});
|
|
207
|
+
|
|
208
|
+
it("caps tracked webhook rate-limit keys to prevent unbounded growth", () => {
|
|
209
|
+
const now = 1_000_000;
|
|
210
|
+
for (let i = 0; i < 4_500; i += 1) {
|
|
211
|
+
isWebhookRateLimitedForTest(`/feishu-rate-limit:key-${i}`, now);
|
|
212
|
+
}
|
|
213
|
+
expect(getFeishuWebhookRateLimitStateSizeForTest()).toBeLessThanOrEqual(4_096);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("prunes stale webhook rate-limit state after window elapses", () => {
|
|
217
|
+
const now = 2_000_000;
|
|
218
|
+
for (let i = 0; i < 100; i += 1) {
|
|
219
|
+
isWebhookRateLimitedForTest(`/feishu-rate-limit-stale:key-${i}`, now);
|
|
220
|
+
}
|
|
221
|
+
expect(getFeishuWebhookRateLimitStateSizeForTest()).toBe(100);
|
|
222
|
+
|
|
223
|
+
isWebhookRateLimitedForTest("/feishu-rate-limit-stale:fresh", now + 60_001);
|
|
224
|
+
expect(getFeishuWebhookRateLimitStateSizeForTest()).toBe(1);
|
|
225
|
+
});
|
|
183
226
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
4
|
+
|
|
5
|
+
describe("feishu onboarding status", () => {
|
|
6
|
+
it("treats SecretRef appSecret as configured when appId is present", async () => {
|
|
7
|
+
const status = await feishuOnboardingAdapter.getStatus({
|
|
8
|
+
cfg: {
|
|
9
|
+
channels: {
|
|
10
|
+
feishu: {
|
|
11
|
+
appId: "cli_a123456",
|
|
12
|
+
appSecret: {
|
|
13
|
+
source: "env",
|
|
14
|
+
provider: "default",
|
|
15
|
+
id: "FEISHU_APP_SECRET",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} as OpenClawConfig,
|
|
20
|
+
accountOverrides: {},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(status.configured).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|