@openclaw/feishu 2026.3.2 → 2026.3.8-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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +199 -13
- package/src/accounts.ts +45 -17
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +8 -0
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +516 -9
- package/src/bot.ts +366 -109
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +52 -64
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +207 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +14 -6
- package/src/config-schema.ts +5 -1
- package/src/dedup.ts +1 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +3 -3
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +60 -13
- package/src/media.ts +23 -9
- package/src/monitor.account.ts +19 -8
- package/src/monitor.reaction.test.ts +111 -105
- package/src/monitor.startup.test.ts +11 -10
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.ts +4 -1
- package/src/monitor.test-mocks.ts +42 -9
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +8 -23
- package/src/onboarding.status.test.ts +1 -1
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +86 -71
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +18 -18
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +175 -0
- package/src/reply-dispatcher.ts +69 -21
- package/src/runtime.ts +5 -13
- package/src/secret-input.ts +8 -14
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +1 -1
- package/src/send-target.ts +1 -1
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.ts +5 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +2 -3
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/media.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { Readable } from "stream";
|
|
4
|
-
import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
4
|
+
import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
5
5
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
6
6
|
import { createFeishuClient } from "./client.js";
|
|
7
7
|
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
|
@@ -9,6 +9,8 @@ import { getFeishuRuntime } from "./runtime.js";
|
|
|
9
9
|
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
|
|
10
10
|
import { resolveFeishuSendTarget } from "./send-target.js";
|
|
11
11
|
|
|
12
|
+
const FEISHU_MEDIA_HTTP_TIMEOUT_MS = 120_000;
|
|
13
|
+
|
|
12
14
|
export type DownloadImageResult = {
|
|
13
15
|
buffer: Buffer;
|
|
14
16
|
contentType?: string;
|
|
@@ -97,7 +99,10 @@ export async function downloadImageFeishu(params: {
|
|
|
97
99
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
const client = createFeishuClient(
|
|
102
|
+
const client = createFeishuClient({
|
|
103
|
+
...account,
|
|
104
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
105
|
+
});
|
|
101
106
|
|
|
102
107
|
const response = await client.im.image.get({
|
|
103
108
|
path: { image_key: normalizedImageKey },
|
|
@@ -132,7 +137,10 @@ export async function downloadMessageResourceFeishu(params: {
|
|
|
132
137
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
const client = createFeishuClient(
|
|
140
|
+
const client = createFeishuClient({
|
|
141
|
+
...account,
|
|
142
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
143
|
+
});
|
|
136
144
|
|
|
137
145
|
const response = await client.im.messageResource.get({
|
|
138
146
|
path: { message_id: messageId, file_key: normalizedFileKey },
|
|
@@ -176,7 +184,10 @@ export async function uploadImageFeishu(params: {
|
|
|
176
184
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
const client = createFeishuClient(
|
|
187
|
+
const client = createFeishuClient({
|
|
188
|
+
...account,
|
|
189
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
190
|
+
});
|
|
180
191
|
|
|
181
192
|
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
|
182
193
|
// Using Readable.from(buffer) causes issues with form-data library
|
|
@@ -243,7 +254,10 @@ export async function uploadFileFeishu(params: {
|
|
|
243
254
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
244
255
|
}
|
|
245
256
|
|
|
246
|
-
const client = createFeishuClient(
|
|
257
|
+
const client = createFeishuClient({
|
|
258
|
+
...account,
|
|
259
|
+
httpTimeoutMs: FEISHU_MEDIA_HTTP_TIMEOUT_MS,
|
|
260
|
+
});
|
|
247
261
|
|
|
248
262
|
// SDK accepts Buffer directly or fs.ReadStream for file paths
|
|
249
263
|
// Using Readable.from(buffer) causes issues with form-data library
|
|
@@ -328,8 +342,8 @@ export async function sendFileFeishu(params: {
|
|
|
328
342
|
cfg: ClawdbotConfig;
|
|
329
343
|
to: string;
|
|
330
344
|
fileKey: string;
|
|
331
|
-
/** Use "audio" for audio
|
|
332
|
-
msgType?: "file" | "audio";
|
|
345
|
+
/** Use "audio" for audio, "media" for video (mp4), "file" for documents */
|
|
346
|
+
msgType?: "file" | "audio" | "media";
|
|
333
347
|
replyToMessageId?: string;
|
|
334
348
|
replyInThread?: boolean;
|
|
335
349
|
accountId?: string;
|
|
@@ -467,8 +481,8 @@ export async function sendMediaFeishu(params: {
|
|
|
467
481
|
fileType,
|
|
468
482
|
accountId,
|
|
469
483
|
});
|
|
470
|
-
// Feishu API: opus -> "audio",
|
|
471
|
-
const msgType = fileType === "opus" ? "audio" : "file";
|
|
484
|
+
// Feishu API: opus -> "audio", mp4/video -> "media" (playable), others -> "file"
|
|
485
|
+
const msgType = fileType === "opus" ? "audio" : fileType === "mp4" ? "media" : "file";
|
|
472
486
|
return sendFileFeishu({
|
|
473
487
|
cfg,
|
|
474
488
|
to,
|
package/src/monitor.account.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
2
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
|
-
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk/feishu";
|
|
4
4
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
5
5
|
import { raceWithTimeoutAndAbort } from "./async.js";
|
|
6
6
|
import {
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
warmupDedupFromDisk,
|
|
20
20
|
} from "./dedup.js";
|
|
21
21
|
import { isMentionForwardRequest } from "./mention.js";
|
|
22
|
-
import {
|
|
23
|
-
import { botOpenIds } from "./monitor.state.js";
|
|
22
|
+
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
|
|
23
|
+
import { botNames, botOpenIds } from "./monitor.state.js";
|
|
24
24
|
import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
|
|
25
25
|
import { getFeishuRuntime } from "./runtime.js";
|
|
26
26
|
import { getMessageFeishu } from "./send.js";
|
|
@@ -247,6 +247,7 @@ function registerEventHandlers(
|
|
|
247
247
|
cfg,
|
|
248
248
|
event,
|
|
249
249
|
botOpenId: botOpenIds.get(accountId),
|
|
250
|
+
botName: botNames.get(accountId),
|
|
250
251
|
runtime,
|
|
251
252
|
chatHistories,
|
|
252
253
|
accountId,
|
|
@@ -260,7 +261,7 @@ function registerEventHandlers(
|
|
|
260
261
|
};
|
|
261
262
|
const resolveDebounceText = (event: FeishuMessageEvent): string => {
|
|
262
263
|
const botOpenId = botOpenIds.get(accountId);
|
|
263
|
-
const parsed = parseFeishuMessageEvent(event, botOpenId);
|
|
264
|
+
const parsed = parseFeishuMessageEvent(event, botOpenId, botNames.get(accountId));
|
|
264
265
|
return parsed.content.trim();
|
|
265
266
|
};
|
|
266
267
|
const recordSuppressedMessageIds = async (
|
|
@@ -430,6 +431,7 @@ function registerEventHandlers(
|
|
|
430
431
|
cfg,
|
|
431
432
|
event: syntheticEvent,
|
|
432
433
|
botOpenId: myBotId,
|
|
434
|
+
botName: botNames.get(accountId),
|
|
433
435
|
runtime,
|
|
434
436
|
chatHistories,
|
|
435
437
|
accountId,
|
|
@@ -483,7 +485,9 @@ function registerEventHandlers(
|
|
|
483
485
|
});
|
|
484
486
|
}
|
|
485
487
|
|
|
486
|
-
export type BotOpenIdSource =
|
|
488
|
+
export type BotOpenIdSource =
|
|
489
|
+
| { kind: "prefetched"; botOpenId?: string; botName?: string }
|
|
490
|
+
| { kind: "fetch" };
|
|
487
491
|
|
|
488
492
|
export type MonitorSingleAccountParams = {
|
|
489
493
|
cfg: ClawdbotConfig;
|
|
@@ -499,11 +503,18 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
|
|
|
499
503
|
const log = runtime?.log ?? console.log;
|
|
500
504
|
|
|
501
505
|
const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
|
|
502
|
-
const
|
|
506
|
+
const botIdentity =
|
|
503
507
|
botOpenIdSource.kind === "prefetched"
|
|
504
|
-
? botOpenIdSource.botOpenId
|
|
505
|
-
: await
|
|
508
|
+
? { botOpenId: botOpenIdSource.botOpenId, botName: botOpenIdSource.botName }
|
|
509
|
+
: await fetchBotIdentityForMonitor(account, { runtime, abortSignal });
|
|
510
|
+
const botOpenId = botIdentity.botOpenId;
|
|
511
|
+
const botName = botIdentity.botName?.trim();
|
|
506
512
|
botOpenIds.set(accountId, botOpenId ?? "");
|
|
513
|
+
if (botName) {
|
|
514
|
+
botNames.set(accountId, botName);
|
|
515
|
+
} else {
|
|
516
|
+
botNames.delete(accountId);
|
|
517
|
+
}
|
|
507
518
|
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
508
519
|
|
|
509
520
|
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { hasControlCommand } from "../../../src/auto-reply/command-detection.js";
|
|
4
4
|
import {
|
|
@@ -51,6 +51,30 @@ function makeReactionEvent(
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function createFetchedReactionMessage(chatId: string) {
|
|
55
|
+
return {
|
|
56
|
+
messageId: "om_msg1",
|
|
57
|
+
chatId,
|
|
58
|
+
senderOpenId: "ou_bot",
|
|
59
|
+
content: "hello",
|
|
60
|
+
contentType: "text",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function resolveReactionWithLookup(params: {
|
|
65
|
+
event?: FeishuReactionCreatedEvent;
|
|
66
|
+
lookupChatId: string;
|
|
67
|
+
}) {
|
|
68
|
+
return await resolveReactionSyntheticEvent({
|
|
69
|
+
cfg,
|
|
70
|
+
accountId: "default",
|
|
71
|
+
event: params.event ?? makeReactionEvent(),
|
|
72
|
+
botOpenId: "ou_bot",
|
|
73
|
+
fetchMessage: async () => createFetchedReactionMessage(params.lookupChatId),
|
|
74
|
+
uuid: () => "fixed-uuid",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
type FeishuMention = NonNullable<FeishuMessageEvent["message"]["mentions"]>[number];
|
|
55
79
|
|
|
56
80
|
function buildDebounceConfig(): ClawdbotConfig {
|
|
@@ -77,7 +101,7 @@ function buildDebounceAccount(): ResolvedFeishuAccount {
|
|
|
77
101
|
enabled: true,
|
|
78
102
|
configured: true,
|
|
79
103
|
appId: "cli_test",
|
|
80
|
-
appSecret: "secret_test",
|
|
104
|
+
appSecret: "secret_test", // pragma: allowlist secret
|
|
81
105
|
domain: "feishu",
|
|
82
106
|
config: {
|
|
83
107
|
enabled: true,
|
|
@@ -109,7 +133,10 @@ function createTextEvent(params: {
|
|
|
109
133
|
};
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
async function setupDebounceMonitor(
|
|
136
|
+
async function setupDebounceMonitor(params?: {
|
|
137
|
+
botOpenId?: string;
|
|
138
|
+
botName?: string;
|
|
139
|
+
}): Promise<(data: unknown) => Promise<void>> {
|
|
113
140
|
const register = vi.fn((registered: Record<string, (data: unknown) => Promise<void>>) => {
|
|
114
141
|
handlers = registered;
|
|
115
142
|
});
|
|
@@ -123,7 +150,11 @@ async function setupDebounceMonitor(): Promise<(data: unknown) => Promise<void>>
|
|
|
123
150
|
error: vi.fn(),
|
|
124
151
|
exit: vi.fn(),
|
|
125
152
|
} as RuntimeEnv,
|
|
126
|
-
botOpenIdSource: {
|
|
153
|
+
botOpenIdSource: {
|
|
154
|
+
kind: "prefetched",
|
|
155
|
+
botOpenId: params?.botOpenId ?? "ou_bot",
|
|
156
|
+
botName: params?.botName,
|
|
157
|
+
},
|
|
127
158
|
});
|
|
128
159
|
|
|
129
160
|
const onMessage = handlers["im.message.receive_v1"];
|
|
@@ -145,6 +176,30 @@ function getFirstDispatchedEvent(): FeishuMessageEvent {
|
|
|
145
176
|
return firstParams.event;
|
|
146
177
|
}
|
|
147
178
|
|
|
179
|
+
function setDedupPassThroughMocks(): void {
|
|
180
|
+
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
|
|
181
|
+
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
|
|
182
|
+
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
|
|
183
|
+
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createMention(params: { openId: string; name: string; key?: string }): FeishuMention {
|
|
187
|
+
return {
|
|
188
|
+
key: params.key ?? "@_user_1",
|
|
189
|
+
id: { open_id: params.openId },
|
|
190
|
+
name: params.name,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function enqueueDebouncedMessage(
|
|
195
|
+
onMessage: (data: unknown) => Promise<void>,
|
|
196
|
+
event: FeishuMessageEvent,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
await onMessage(event);
|
|
199
|
+
await Promise.resolve();
|
|
200
|
+
await Promise.resolve();
|
|
201
|
+
}
|
|
202
|
+
|
|
148
203
|
describe("resolveReactionSyntheticEvent", () => {
|
|
149
204
|
it("filters app self-reactions", async () => {
|
|
150
205
|
const event = makeReactionEvent({ operator_type: "app" });
|
|
@@ -265,23 +320,12 @@ describe("resolveReactionSyntheticEvent", () => {
|
|
|
265
320
|
});
|
|
266
321
|
|
|
267
322
|
it("uses event chat context when provided", async () => {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const result = await resolveReactionSyntheticEvent({
|
|
273
|
-
cfg,
|
|
274
|
-
accountId: "default",
|
|
275
|
-
event,
|
|
276
|
-
botOpenId: "ou_bot",
|
|
277
|
-
fetchMessage: async () => ({
|
|
278
|
-
messageId: "om_msg1",
|
|
279
|
-
chatId: "oc_group_from_lookup",
|
|
280
|
-
senderOpenId: "ou_bot",
|
|
281
|
-
content: "hello",
|
|
282
|
-
contentType: "text",
|
|
323
|
+
const result = await resolveReactionWithLookup({
|
|
324
|
+
event: makeReactionEvent({
|
|
325
|
+
chat_id: "oc_group_from_event",
|
|
326
|
+
chat_type: "group",
|
|
283
327
|
}),
|
|
284
|
-
|
|
328
|
+
lookupChatId: "oc_group_from_lookup",
|
|
285
329
|
});
|
|
286
330
|
|
|
287
331
|
expect(result).toEqual({
|
|
@@ -302,20 +346,8 @@ describe("resolveReactionSyntheticEvent", () => {
|
|
|
302
346
|
});
|
|
303
347
|
|
|
304
348
|
it("falls back to reacted message chat_id when event chat_id is absent", async () => {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
cfg,
|
|
308
|
-
accountId: "default",
|
|
309
|
-
event,
|
|
310
|
-
botOpenId: "ou_bot",
|
|
311
|
-
fetchMessage: async () => ({
|
|
312
|
-
messageId: "om_msg1",
|
|
313
|
-
chatId: "oc_group_from_lookup",
|
|
314
|
-
senderOpenId: "ou_bot",
|
|
315
|
-
content: "hello",
|
|
316
|
-
contentType: "text",
|
|
317
|
-
}),
|
|
318
|
-
uuid: () => "fixed-uuid",
|
|
349
|
+
const result = await resolveReactionWithLookup({
|
|
350
|
+
lookupChatId: "oc_group_from_lookup",
|
|
319
351
|
});
|
|
320
352
|
|
|
321
353
|
expect(result?.message.chat_id).toBe("oc_group_from_lookup");
|
|
@@ -323,20 +355,8 @@ describe("resolveReactionSyntheticEvent", () => {
|
|
|
323
355
|
});
|
|
324
356
|
|
|
325
357
|
it("falls back to sender p2p chat when lookup returns empty chat_id", async () => {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
cfg,
|
|
329
|
-
accountId: "default",
|
|
330
|
-
event,
|
|
331
|
-
botOpenId: "ou_bot",
|
|
332
|
-
fetchMessage: async () => ({
|
|
333
|
-
messageId: "om_msg1",
|
|
334
|
-
chatId: "",
|
|
335
|
-
senderOpenId: "ou_bot",
|
|
336
|
-
content: "hello",
|
|
337
|
-
contentType: "text",
|
|
338
|
-
}),
|
|
339
|
-
uuid: () => "fixed-uuid",
|
|
358
|
+
const result = await resolveReactionWithLookup({
|
|
359
|
+
lookupChatId: "",
|
|
340
360
|
});
|
|
341
361
|
|
|
342
362
|
expect(result?.message.chat_id).toBe("p2p:ou_user1");
|
|
@@ -389,42 +409,25 @@ describe("Feishu inbound debounce regressions", () => {
|
|
|
389
409
|
});
|
|
390
410
|
|
|
391
411
|
it("keeps bot mention when per-message mention keys collide across non-forward messages", async () => {
|
|
392
|
-
|
|
393
|
-
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
|
|
394
|
-
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
|
|
395
|
-
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
|
|
412
|
+
setDedupPassThroughMocks();
|
|
396
413
|
const onMessage = await setupDebounceMonitor();
|
|
397
414
|
|
|
398
|
-
await
|
|
415
|
+
await enqueueDebouncedMessage(
|
|
416
|
+
onMessage,
|
|
399
417
|
createTextEvent({
|
|
400
418
|
messageId: "om_1",
|
|
401
419
|
text: "first",
|
|
402
|
-
mentions: [
|
|
403
|
-
{
|
|
404
|
-
key: "@_user_1",
|
|
405
|
-
id: { open_id: "ou_user_a" },
|
|
406
|
-
name: "user-a",
|
|
407
|
-
},
|
|
408
|
-
],
|
|
420
|
+
mentions: [createMention({ openId: "ou_user_a", name: "user-a" })],
|
|
409
421
|
}),
|
|
410
422
|
);
|
|
411
|
-
await
|
|
412
|
-
|
|
413
|
-
await onMessage(
|
|
423
|
+
await enqueueDebouncedMessage(
|
|
424
|
+
onMessage,
|
|
414
425
|
createTextEvent({
|
|
415
426
|
messageId: "om_2",
|
|
416
427
|
text: "@bot second",
|
|
417
|
-
mentions: [
|
|
418
|
-
{
|
|
419
|
-
key: "@_user_1",
|
|
420
|
-
id: { open_id: "ou_bot" },
|
|
421
|
-
name: "bot",
|
|
422
|
-
},
|
|
423
|
-
],
|
|
428
|
+
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
|
|
424
429
|
}),
|
|
425
430
|
);
|
|
426
|
-
await Promise.resolve();
|
|
427
|
-
await Promise.resolve();
|
|
428
431
|
await vi.advanceTimersByTimeAsync(25);
|
|
429
432
|
|
|
430
433
|
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
@@ -434,43 +437,57 @@ describe("Feishu inbound debounce regressions", () => {
|
|
|
434
437
|
expect(mergedMentions.some((mention) => mention.id.open_id === "ou_user_a")).toBe(false);
|
|
435
438
|
});
|
|
436
439
|
|
|
437
|
-
it("
|
|
440
|
+
it("passes prefetched botName through to handleFeishuMessage", async () => {
|
|
438
441
|
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
|
|
439
442
|
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
|
|
440
443
|
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
|
|
441
444
|
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
|
|
442
|
-
const onMessage = await setupDebounceMonitor();
|
|
445
|
+
const onMessage = await setupDebounceMonitor({ botName: "OpenClaw Bot" });
|
|
443
446
|
|
|
444
447
|
await onMessage(
|
|
445
448
|
createTextEvent({
|
|
446
|
-
messageId: "
|
|
447
|
-
text: "@
|
|
449
|
+
messageId: "om_name_passthrough",
|
|
450
|
+
text: "@bot hello",
|
|
448
451
|
mentions: [
|
|
449
452
|
{
|
|
450
453
|
key: "@_user_1",
|
|
451
|
-
id: { open_id: "
|
|
452
|
-
name: "
|
|
454
|
+
id: { open_id: "ou_bot" },
|
|
455
|
+
name: "OpenClaw Bot",
|
|
453
456
|
},
|
|
454
457
|
],
|
|
455
458
|
}),
|
|
456
459
|
);
|
|
457
460
|
await Promise.resolve();
|
|
458
461
|
await Promise.resolve();
|
|
459
|
-
await
|
|
462
|
+
await vi.advanceTimersByTimeAsync(25);
|
|
463
|
+
|
|
464
|
+
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
465
|
+
const firstParams = handleFeishuMessageMock.mock.calls[0]?.[0] as
|
|
466
|
+
| { botName?: string }
|
|
467
|
+
| undefined;
|
|
468
|
+
expect(firstParams?.botName).toBe("OpenClaw Bot");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("does not synthesize mention-forward intent across separate messages", async () => {
|
|
472
|
+
setDedupPassThroughMocks();
|
|
473
|
+
const onMessage = await setupDebounceMonitor();
|
|
474
|
+
|
|
475
|
+
await enqueueDebouncedMessage(
|
|
476
|
+
onMessage,
|
|
477
|
+
createTextEvent({
|
|
478
|
+
messageId: "om_user_mention",
|
|
479
|
+
text: "@alice first",
|
|
480
|
+
mentions: [createMention({ openId: "ou_alice", name: "alice" })],
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
await enqueueDebouncedMessage(
|
|
484
|
+
onMessage,
|
|
460
485
|
createTextEvent({
|
|
461
486
|
messageId: "om_bot_mention",
|
|
462
487
|
text: "@bot second",
|
|
463
|
-
mentions: [
|
|
464
|
-
{
|
|
465
|
-
key: "@_user_1",
|
|
466
|
-
id: { open_id: "ou_bot" },
|
|
467
|
-
name: "bot",
|
|
468
|
-
},
|
|
469
|
-
],
|
|
488
|
+
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
|
|
470
489
|
}),
|
|
471
490
|
);
|
|
472
|
-
await Promise.resolve();
|
|
473
|
-
await Promise.resolve();
|
|
474
491
|
await vi.advanceTimersByTimeAsync(25);
|
|
475
492
|
|
|
476
493
|
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
@@ -483,35 +500,24 @@ describe("Feishu inbound debounce regressions", () => {
|
|
|
483
500
|
});
|
|
484
501
|
|
|
485
502
|
it("preserves bot mention signal when the latest merged message has no mentions", async () => {
|
|
486
|
-
|
|
487
|
-
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
|
|
488
|
-
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
|
|
489
|
-
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
|
|
503
|
+
setDedupPassThroughMocks();
|
|
490
504
|
const onMessage = await setupDebounceMonitor();
|
|
491
505
|
|
|
492
|
-
await
|
|
506
|
+
await enqueueDebouncedMessage(
|
|
507
|
+
onMessage,
|
|
493
508
|
createTextEvent({
|
|
494
509
|
messageId: "om_bot_first",
|
|
495
510
|
text: "@bot first",
|
|
496
|
-
mentions: [
|
|
497
|
-
{
|
|
498
|
-
key: "@_user_1",
|
|
499
|
-
id: { open_id: "ou_bot" },
|
|
500
|
-
name: "bot",
|
|
501
|
-
},
|
|
502
|
-
],
|
|
511
|
+
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
|
|
503
512
|
}),
|
|
504
513
|
);
|
|
505
|
-
await
|
|
506
|
-
|
|
507
|
-
await onMessage(
|
|
514
|
+
await enqueueDebouncedMessage(
|
|
515
|
+
onMessage,
|
|
508
516
|
createTextEvent({
|
|
509
517
|
messageId: "om_plain_second",
|
|
510
518
|
text: "plain follow-up",
|
|
511
519
|
}),
|
|
512
520
|
);
|
|
513
|
-
await Promise.resolve();
|
|
514
|
-
await Promise.resolve();
|
|
515
521
|
await vi.advanceTimersByTimeAsync(25);
|
|
516
522
|
|
|
517
523
|
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
|
|
4
4
|
|
|
5
5
|
const probeFeishuMock = vi.hoisted(() => vi.fn());
|
|
6
|
-
|
|
7
|
-
vi.mock("./probe.js", () => ({
|
|
8
|
-
probeFeishu: probeFeishuMock,
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock("./client.js", () => ({
|
|
6
|
+
const feishuClientMockModule = vi.hoisted(() => ({
|
|
12
7
|
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
|
|
13
8
|
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
|
|
14
9
|
}));
|
|
15
|
-
|
|
16
|
-
vi.mock("./runtime.js", () => ({
|
|
10
|
+
const feishuRuntimeMockModule = vi.hoisted(() => ({
|
|
17
11
|
getFeishuRuntime: () => ({
|
|
18
12
|
channel: {
|
|
19
13
|
debounce: {
|
|
@@ -30,6 +24,13 @@ vi.mock("./runtime.js", () => ({
|
|
|
30
24
|
}),
|
|
31
25
|
}));
|
|
32
26
|
|
|
27
|
+
vi.mock("./probe.js", () => ({
|
|
28
|
+
probeFeishu: probeFeishuMock,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("./client.js", () => feishuClientMockModule);
|
|
32
|
+
vi.mock("./runtime.js", () => feishuRuntimeMockModule);
|
|
33
|
+
|
|
33
34
|
function buildMultiAccountWebsocketConfig(accountIds: string[]): ClawdbotConfig {
|
|
34
35
|
return {
|
|
35
36
|
channels: {
|
|
@@ -41,7 +42,7 @@ function buildMultiAccountWebsocketConfig(accountIds: string[]): ClawdbotConfig
|
|
|
41
42
|
{
|
|
42
43
|
enabled: true,
|
|
43
44
|
appId: `cli_${accountId}`,
|
|
44
|
-
appSecret: `secret_${accountId}`,
|
|
45
|
+
appSecret: `secret_${accountId}`, // pragma: allowlist secret
|
|
45
46
|
connectionMode: "websocket",
|
|
46
47
|
},
|
|
47
48
|
]),
|
package/src/monitor.startup.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { probeFeishu } from "./probe.js";
|
|
3
3
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
4
4
|
|
|
@@ -10,6 +10,11 @@ type FetchBotOpenIdOptions = {
|
|
|
10
10
|
timeoutMs?: number;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export type FeishuMonitorBotIdentity = {
|
|
14
|
+
botOpenId?: string;
|
|
15
|
+
botName?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
13
18
|
function isTimeoutErrorMessage(message: string | undefined): boolean {
|
|
14
19
|
return message?.toLowerCase().includes("timeout") || message?.toLowerCase().includes("timed out")
|
|
15
20
|
? true
|
|
@@ -20,12 +25,12 @@ function isAbortErrorMessage(message: string | undefined): boolean {
|
|
|
20
25
|
return message?.toLowerCase().includes("aborted") ?? false;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
export async function
|
|
28
|
+
export async function fetchBotIdentityForMonitor(
|
|
24
29
|
account: ResolvedFeishuAccount,
|
|
25
30
|
options: FetchBotOpenIdOptions = {},
|
|
26
|
-
): Promise<
|
|
31
|
+
): Promise<FeishuMonitorBotIdentity> {
|
|
27
32
|
if (options.abortSignal?.aborted) {
|
|
28
|
-
return
|
|
33
|
+
return {};
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const timeoutMs = options.timeoutMs ?? FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS;
|
|
@@ -34,11 +39,11 @@ export async function fetchBotOpenIdForMonitor(
|
|
|
34
39
|
abortSignal: options.abortSignal,
|
|
35
40
|
});
|
|
36
41
|
if (result.ok) {
|
|
37
|
-
return result.botOpenId;
|
|
42
|
+
return { botOpenId: result.botOpenId, botName: result.botName };
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
if (options.abortSignal?.aborted || isAbortErrorMessage(result.error)) {
|
|
41
|
-
return
|
|
46
|
+
return {};
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
if (isTimeoutErrorMessage(result.error)) {
|
|
@@ -47,5 +52,13 @@ export async function fetchBotOpenIdForMonitor(
|
|
|
47
52
|
`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`,
|
|
48
53
|
);
|
|
49
54
|
}
|
|
50
|
-
return
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function fetchBotOpenIdForMonitor(
|
|
59
|
+
account: ResolvedFeishuAccount,
|
|
60
|
+
options: FetchBotOpenIdOptions = {},
|
|
61
|
+
): Promise<string | undefined> {
|
|
62
|
+
const identity = await fetchBotIdentityForMonitor(account, options);
|
|
63
|
+
return identity.botOpenId;
|
|
51
64
|
}
|
package/src/monitor.state.ts
CHANGED
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
type RuntimeEnv,
|
|
7
7
|
WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK,
|
|
8
8
|
WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
9
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
10
10
|
|
|
11
11
|
export const wsClients = new Map<string, Lark.WSClient>();
|
|
12
12
|
export const httpServers = new Map<string, http.Server>();
|
|
13
13
|
export const botOpenIds = new Map<string, string>();
|
|
14
|
+
export const botNames = new Map<string, string>();
|
|
14
15
|
|
|
15
16
|
export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
16
17
|
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
|
@@ -140,6 +141,7 @@ export function stopFeishuMonitorState(accountId?: string): void {
|
|
|
140
141
|
httpServers.delete(accountId);
|
|
141
142
|
}
|
|
142
143
|
botOpenIds.delete(accountId);
|
|
144
|
+
botNames.delete(accountId);
|
|
143
145
|
return;
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -149,4 +151,5 @@ export function stopFeishuMonitorState(accountId?: string): void {
|
|
|
149
151
|
}
|
|
150
152
|
httpServers.clear();
|
|
151
153
|
botOpenIds.clear();
|
|
154
|
+
botNames.clear();
|
|
152
155
|
}
|