@nextclaw/channel-plugin-feishu 0.2.21 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-plugin-feishu",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "private": false,
5
5
  "description": "NextClaw Feishu/Lark channel plugin with doc/wiki/drive tools.",
6
6
  "type": "module",
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 resolveFeishuSenderName({
986
+ const senderResult = await resolveFeishuSenderNameWithinBudget({
945
987
  account,
946
988
  senderId: ctx.senderOpenId,
947
989
  log,
package/src/channel.ts CHANGED
@@ -109,7 +109,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
109
109
  },
110
110
  agentPrompt: {
111
111
  messageToolHints: () => [
112
- "- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
112
+ "- Feishu targeting: omit `target` only when replying in the current Feishu conversation. For proactive sends from UI/CLI/another channel, pass an explicit target such as `user:open_id` or `chat:chat_id`.",
113
+ "- If the current session is not Feishu, never rely on `channel=feishu` alone; resolve the route first (for example from an existing Feishu session) and then send to that explicit target.",
113
114
  "- Feishu supports interactive cards for rich messages.",
114
115
  ],
115
116
  },