@nextclaw/channel-plugin-feishu 0.2.22 → 0.2.24
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/package.json +1 -1
- package/src/bot.test.ts +45 -0
- package/src/bot.ts +43 -1
package/package.json
CHANGED
package/src/bot.test.ts
CHANGED
|
@@ -342,6 +342,51 @@ describe("handleFeishuMessage command authorization", () => {
|
|
|
342
342
|
expect(mockCreateFeishuClient).not.toHaveBeenCalled();
|
|
343
343
|
});
|
|
344
344
|
|
|
345
|
+
it("does not block reply dispatch when sender-name lookup hangs", async () => {
|
|
346
|
+
vi.useFakeTimers();
|
|
347
|
+
try {
|
|
348
|
+
mockCreateFeishuClient.mockReturnValue({
|
|
349
|
+
contact: {
|
|
350
|
+
user: {
|
|
351
|
+
get: vi.fn(() => new Promise(() => undefined)),
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const cfg: ClawdbotConfig = {
|
|
357
|
+
channels: {
|
|
358
|
+
feishu: {
|
|
359
|
+
dmPolicy: "open",
|
|
360
|
+
allowFrom: ["*"],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
} as ClawdbotConfig;
|
|
364
|
+
|
|
365
|
+
const event: FeishuMessageEvent = {
|
|
366
|
+
sender: {
|
|
367
|
+
sender_id: {
|
|
368
|
+
open_id: "ou-attacker",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
message: {
|
|
372
|
+
message_id: "msg-slow-sender-lookup",
|
|
373
|
+
chat_id: "oc-dm",
|
|
374
|
+
chat_type: "p2p",
|
|
375
|
+
message_type: "text",
|
|
376
|
+
content: JSON.stringify({ text: "hello" }),
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const dispatchPromise = dispatchMessage({ cfg, event });
|
|
381
|
+
await vi.advanceTimersByTimeAsync(1_500);
|
|
382
|
+
await dispatchPromise;
|
|
383
|
+
|
|
384
|
+
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
|
|
385
|
+
} finally {
|
|
386
|
+
vi.useRealTimers();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
345
390
|
it("propagates parent/root message ids into inbound context for reply reconstruction", async () => {
|
|
346
391
|
mockGetMessageFeishu.mockResolvedValueOnce({
|
|
347
392
|
messageId: "om_parent_001",
|
package/src/bot.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
warnMissingProviderGroupPolicyFallbackOnce,
|
|
15
15
|
} from "./nextclaw-sdk/feishu.js";
|
|
16
16
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
17
|
+
import { raceWithTimeoutAndAbort } from "./async.js";
|
|
17
18
|
import { createFeishuClient } from "./client.js";
|
|
18
19
|
import { finalizeFeishuMessageProcessing, tryRecordMessagePersistent } from "./dedup.js";
|
|
19
20
|
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
|
@@ -96,6 +97,7 @@ function extractPermissionError(err: unknown): PermissionError | null {
|
|
|
96
97
|
// --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
|
|
97
98
|
// Cache display names by sender id (open_id/user_id) to avoid an API call on every message.
|
|
98
99
|
const SENDER_NAME_TTL_MS = 10 * 60 * 1000;
|
|
100
|
+
const SENDER_NAME_LOOKUP_BUDGET_MS = 1_500;
|
|
99
101
|
const senderNameCache = new Map<string, { name: string; expireAt: number }>();
|
|
100
102
|
|
|
101
103
|
// Cache permission errors to avoid spamming the user with repeated notifications.
|
|
@@ -108,6 +110,20 @@ type SenderNameResult = {
|
|
|
108
110
|
permissionError?: PermissionError;
|
|
109
111
|
};
|
|
110
112
|
|
|
113
|
+
function getCachedSenderName(senderId: string): string | undefined {
|
|
114
|
+
const normalizedSenderId = senderId.trim();
|
|
115
|
+
if (!normalizedSenderId) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cached = senderNameCache.get(normalizedSenderId);
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
if (!cached || cached.expireAt <= now) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return cached.name;
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
function resolveSenderLookupIdType(senderId: string): "open_id" | "user_id" | "union_id" {
|
|
112
128
|
const trimmed = senderId.trim();
|
|
113
129
|
if (trimmed.startsWith("ou_")) {
|
|
@@ -174,6 +190,32 @@ async function resolveFeishuSenderName(params: {
|
|
|
174
190
|
}
|
|
175
191
|
}
|
|
176
192
|
|
|
193
|
+
async function resolveFeishuSenderNameWithinBudget(params: {
|
|
194
|
+
account: ResolvedFeishuAccount;
|
|
195
|
+
senderId: string;
|
|
196
|
+
log: (...args: any[]) => void;
|
|
197
|
+
timeoutMs?: number;
|
|
198
|
+
}): Promise<SenderNameResult> {
|
|
199
|
+
const cachedName = getCachedSenderName(params.senderId);
|
|
200
|
+
if (cachedName) {
|
|
201
|
+
return { name: cachedName };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const timeoutMs = params.timeoutMs ?? SENDER_NAME_LOOKUP_BUDGET_MS;
|
|
205
|
+
const lookupPromise = resolveFeishuSenderName(params);
|
|
206
|
+
const lookupResult = await raceWithTimeoutAndAbort(lookupPromise, { timeoutMs });
|
|
207
|
+
if (lookupResult.status === "resolved") {
|
|
208
|
+
return lookupResult.value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
params.log(
|
|
212
|
+
`feishu[${params.account.accountId}]: sender-name lookup exceeded ${timeoutMs}ms; ` +
|
|
213
|
+
"continuing without blocking reply",
|
|
214
|
+
);
|
|
215
|
+
void lookupPromise.catch(() => undefined);
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
|
|
177
219
|
export type FeishuMessageEvent = {
|
|
178
220
|
sender: {
|
|
179
221
|
sender_id: {
|
|
@@ -941,7 +983,7 @@ export async function handleFeishuMessage(params: {
|
|
|
941
983
|
// Optimization: skip if disabled to save API quota (Feishu free tier limit).
|
|
942
984
|
let permissionErrorForAgent: PermissionError | undefined;
|
|
943
985
|
if (feishuCfg?.resolveSenderNames ?? true) {
|
|
944
|
-
const senderResult = await
|
|
986
|
+
const senderResult = await resolveFeishuSenderNameWithinBudget({
|
|
945
987
|
account,
|
|
946
988
|
senderId: ctx.senderOpenId,
|
|
947
989
|
log,
|