@lmcl/ailo-mcp-feishu 0.1.0 → 0.2.0

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.
@@ -40,6 +40,16 @@ export class FeishuHandler {
40
40
  get storage() {
41
41
  return this.ctx?.storage ?? null;
42
42
  }
43
+ /** 通过 WS 发给 Aido 代打,ctx 未就绪时回退到 console */
44
+ _log(level, message, data) {
45
+ if (this.ctx?.log) {
46
+ this.ctx.log(level, message, data);
47
+ }
48
+ else {
49
+ const fn = level === "error" ? console.error : level === "warn" ? console.warn : level === "debug" ? console.debug : console.log;
50
+ fn(`[feishu] ${message}`, data ?? "");
51
+ }
52
+ }
43
53
  /** 外部用户数据单 key:{ _counter, [userId]: label } */
44
54
  static EXTERNAL_USERS_KEY = "external_users";
45
55
  /**
@@ -61,14 +71,14 @@ export class FeishuHandler {
61
71
  if (k !== "_counter" && typeof v === "string")
62
72
  this.externalUserLabels.set(k, v);
63
73
  }
64
- console.error(`[feishu] 已加载外部用户映射: ${this.externalUserLabels.size} 条, counter=${this.externalUserCounter}`);
74
+ this._log("info", `已加载外部用户映射: ${this.externalUserLabels.size} 条, counter=${this.externalUserCounter}`);
65
75
  }
66
76
  catch {
67
- console.warn("[feishu] 解析外部用户数据失败,将从零开始");
77
+ this._log("warn", "解析外部用户数据失败,将从零开始");
68
78
  }
69
79
  })
70
80
  .catch((err) => {
71
- console.warn("[feishu] 加载外部用户映射失败,将从零开始:", err);
81
+ this._log("warn", "加载外部用户映射失败,将从零开始", { err: String(err) });
72
82
  });
73
83
  }
74
84
  /**
@@ -87,7 +97,7 @@ export class FeishuHandler {
87
97
  return this.storage.setData(FeishuHandler.EXTERNAL_USERS_KEY, JSON.stringify(obj));
88
98
  })
89
99
  .catch((err) => {
90
- console.warn("[feishu] 保存外部用户映射失败:", err);
100
+ this._log("warn", "保存外部用户映射失败", { err: String(err) });
91
101
  });
92
102
  }
93
103
  /**
@@ -107,13 +117,13 @@ export class FeishuHandler {
107
117
  const parsed = new URL(url);
108
118
  const pathParts = parsed.pathname.split("/").filter(Boolean);
109
119
  if (pathParts.length < 2) {
110
- console.warn("[feishu] fetchDocWithImageRefs: invalid URL path", url);
120
+ this._log("warn", "fetchDocWithImageRefs: invalid URL path", { url });
111
121
  return null;
112
122
  }
113
123
  const type = pathParts[0];
114
124
  const id = pathParts[1];
115
125
  if (!id) {
116
- console.warn("[feishu] fetchDocWithImageRefs: missing doc id", url);
126
+ this._log("warn", "fetchDocWithImageRefs: missing doc id", { url });
117
127
  return null;
118
128
  }
119
129
  if (type === "docx") {
@@ -132,14 +142,14 @@ export class FeishuHandler {
132
142
  const docContent = await this.fetchDocRawContentLegacy(id);
133
143
  if (docContent !== null)
134
144
  return docContent;
135
- console.warn("[feishu] fetchDocWithImageRefs docs failed for both APIs");
145
+ this._log("warn", "fetchDocWithImageRefs docs failed for both APIs");
136
146
  return null;
137
147
  }
138
- console.warn("[feishu] fetchDocWithImageRefs: unsupported doc type", type);
148
+ this._log("warn", "fetchDocWithImageRefs: unsupported doc type", { type });
139
149
  return null;
140
150
  }
141
151
  catch (err) {
142
- console.warn("[feishu] fetchDocWithImageRefs error:", err);
152
+ this._log("warn", "fetchDocWithImageRefs error", { err: String(err) });
143
153
  return null;
144
154
  }
145
155
  }
@@ -150,7 +160,7 @@ export class FeishuHandler {
150
160
  data: {},
151
161
  }));
152
162
  if (res?.code !== 0) {
153
- console.warn("[feishu] fetchDocxRawContent failed:", res);
163
+ this._log("warn", "fetchDocxRawContent failed", { code: res?.code });
154
164
  return null;
155
165
  }
156
166
  return res.data?.content ?? null;
@@ -162,7 +172,7 @@ export class FeishuHandler {
162
172
  data: {},
163
173
  }));
164
174
  if (res?.code !== 0) {
165
- console.warn("[feishu] fetchDocRawContentLegacy failed:", res);
175
+ this._log("warn", "fetchDocRawContentLegacy failed", { code: res?.code });
166
176
  return null;
167
177
  }
168
178
  return res.data?.content ?? null;
@@ -181,7 +191,7 @@ export class FeishuHandler {
181
191
  }));
182
192
  if (res?.code !== 0 || !res.data?.items) {
183
193
  if (res?.code !== 0)
184
- console.warn("[feishu] docx blocks failed:", res);
194
+ this._log("warn", "docx blocks failed", { code: res?.code });
185
195
  return false;
186
196
  }
187
197
  for (const block of res.data.items) {
@@ -228,7 +238,7 @@ export class FeishuHandler {
228
238
  data: { file_tokens: fileTokens },
229
239
  }));
230
240
  if (urlRes?.code !== 0 || !urlRes.data?.tmp_download_urls) {
231
- console.warn("[feishu] batch_get_tmp_download_url failed:", urlRes);
241
+ this._log("warn", "batch_get_tmp_download_url failed", { code: urlRes?.code });
232
242
  return parts.join("").replace(/\[图片 \d+\]\(view_image_url_placeholder_[^)]+\)/g, "[图片]") || null;
233
243
  }
234
244
  const urlMap = new Map();
@@ -253,12 +263,12 @@ export class FeishuHandler {
253
263
  const bot = res.data?.bot;
254
264
  this.botOpenId = bot?.open_id ?? bot?.user_id ?? "";
255
265
  if (this.botOpenId) {
256
- console.error(`[feishu] bot open_id: ${this.botOpenId}`);
266
+ this._log("info", `bot open_id: ${this.botOpenId}`);
257
267
  }
258
268
  // 获取失败时仅 debug 级别,不影响核心功能(仅群聊 @检测 会失效)
259
269
  }
260
270
  catch (err) {
261
- console.warn("[feishu] failed to fetch bot info:", err);
271
+ this._log("warn", "failed to fetch bot info", { err: String(err) });
262
272
  }
263
273
  }
264
274
  resolveMentions(text, mentions) {
@@ -287,7 +297,7 @@ export class FeishuHandler {
287
297
  acceptMessage(msg) {
288
298
  if (!this.ctx)
289
299
  return;
290
- this.ctx.accept(msg).catch((err) => console.error("[feishu] accept failed:", err));
300
+ this.ctx.accept(msg).catch((err) => this._log("error", "accept failed", { err: String(err) }));
291
301
  }
292
302
  setOnP2PChatEntered(handler) {
293
303
  this.onP2PChatEntered = handler;
@@ -375,7 +385,7 @@ export class FeishuHandler {
375
385
  const user = res.data?.user;
376
386
  const resolvedName = user?.name || user?.en_name || user?.nickname || "";
377
387
  if (!resolvedName) {
378
- console.warn(`[feishu] getUserInfo(${userId}): 所有名称字段为空,应用可能缺少 contact:user.base:readonly 权限`);
388
+ this._log("warn", `getUserInfo(${userId}): 所有名称字段为空,应用可能缺少 contact:user.base:readonly 权限`);
379
389
  }
380
390
  const info = {
381
391
  name: resolvedName,
@@ -390,11 +400,11 @@ export class FeishuHandler {
390
400
  const errCode = this.extractFeishuErrorCode(err);
391
401
  if (errCode === 41050) {
392
402
  // 41050 = no user authority:外部用户(非本租户成员),无权访问通讯录信息,属预期情况
393
- console.error(`[feishu] getUserInfo(${userId}): 外部用户,无权限获取通讯录信息 (41050)`);
403
+ this._log("info", `getUserInfo(${userId}): 外部用户,无权限获取通讯录信息 (41050)`);
394
404
  }
395
405
  else {
396
406
  const detail = err?.response?.data ?? err.message;
397
- console.warn(`[feishu] failed to get user ${userId}:`, detail);
407
+ this._log("warn", `failed to get user ${userId}`, { detail });
398
408
  }
399
409
  // 缓存失败结果(短 TTL),避免对同一用户重复调用失败的 API
400
410
  // 为每个外部用户分配稳定的顺序编号(如「外部用户1」「外部用户2」),帮助 LLM 区分
@@ -438,12 +448,11 @@ export class FeishuHandler {
438
448
  catch (err) {
439
449
  const errCode = this.extractFeishuErrorCode(err);
440
450
  if (errCode === 41050) {
441
- // 41050 = no user authority:外部群可能无权获取群信息
442
- console.error(`[feishu] getChatInfo(${chatId}): 无权限获取群信息 (41050)`);
451
+ this._log("info", `getChatInfo(${chatId}): 无权限获取群信息 (41050)`);
443
452
  }
444
453
  else {
445
454
  const detail = err?.response?.data ?? err.message;
446
- console.warn(`[feishu] failed to get chat ${chatId}:`, detail);
455
+ this._log("warn", `failed to get chat ${chatId}`, { detail });
447
456
  }
448
457
  // 缓存失败结果(短 TTL),避免对同一群聊重复调用失败的 API
449
458
  const fallback = { name: chatId };
@@ -469,7 +478,7 @@ export class FeishuHandler {
469
478
  this.chatCache.delete(key);
470
479
  }
471
480
  }
472
- console.error(`[feishu] cache cleaned: users=${this.userCache.size}, chats=${this.chatCache.size}`);
481
+ this._log("debug", `cache cleaned: users=${this.userCache.size}, chats=${this.chatCache.size}`);
473
482
  }
474
483
  /**
475
484
  * 推断 chatId 对应的聊天类型(撤回/Reaction 等事件不返回 chat_type 时使用)
@@ -525,18 +534,23 @@ export class FeishuHandler {
525
534
  /**
526
535
  * 使用飞书 WebSocket 长连接接收事件,无需配置回调地址。
527
536
  * 需在飞书开放平台「事件与回调」中选择「使用长连接接收事件」并保存(本客户端需在线)。
537
+ * 必须 await 飞书连接就绪后返回,避免 Aido 在飞书未连接时就认为通道就绪导致消息丢失。
528
538
  */
529
- start(ctx) {
539
+ async start(ctx) {
530
540
  this.ctx = ctx;
531
541
  this.loadExternalUserLabels();
532
542
  this.fetchBotOpenId();
533
- // MCP stdio 占用 stdout,Lark SDK logger 必须写到 stderr
543
+ // MCP stdio 占用 stdout,Lark SDK 日志通过 WS 发给 Aido 代打
544
+ const sink = (level) => (...args) => {
545
+ const msg = args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ");
546
+ this._log(level, msg);
547
+ };
534
548
  const stderrLogger = {
535
- error: (...args) => console.error(...args),
536
- warn: (...args) => console.error(...args),
537
- info: (...args) => console.error(...args),
538
- debug: (...args) => console.error(...args),
539
- trace: (...args) => console.error(...args),
549
+ error: sink("error"),
550
+ warn: sink("warn"),
551
+ info: sink("info"),
552
+ debug: sink("debug"),
553
+ trace: sink("debug"),
540
554
  };
541
555
  const wsClient = new Lark.WSClient({
542
556
  appId: this.config.appId,
@@ -552,12 +566,23 @@ export class FeishuHandler {
552
566
  });
553
567
  // 启动定时清理过期缓存(每小时)
554
568
  const cacheCleanupInterval = setInterval(() => this.cleanExpiredCache(), 60 * 60 * 1000);
569
+ // 必须 await 飞书连接就绪后再返回,否则 SDK 会在飞书未连接时就连到 Aido,导致启动窗口期消息丢失
570
+ const wsClientAny = wsClient;
571
+ wsClientAny.eventDispatcher = eventDispatcher;
555
572
  eventDispatcher.register({
556
573
  "im.message.receive_v1": async (data) => {
557
574
  const event = data;
558
575
  if (!event.message || !this.ctx)
559
576
  return;
560
577
  const msg = event.message;
578
+ const rawContent = msg.content ?? "";
579
+ const preview = rawContent.length > 100 ? String(rawContent).slice(0, 100) + "..." : rawContent;
580
+ this._log("debug", "im.message.receive_v1 ENTRY", {
581
+ message_id: msg.message_id ?? "?",
582
+ chat_id: msg.chat_id ?? "?",
583
+ create_time: msg.create_time ?? "?",
584
+ content_preview: preview,
585
+ });
561
586
  const chatId = msg.chat_id ?? "";
562
587
  const messageId = msg.message_id ?? "";
563
588
  const chatType = msg.chat_type === "group" ? "group" : "p2p";
@@ -565,28 +590,27 @@ export class FeishuHandler {
565
590
  const senderId = event.sender?.sender_id?.open_id ??
566
591
  event.sender?.sender_id?.user_id ??
567
592
  "";
568
- const rawContent = msg.content ?? "";
569
593
  const messageType = msg.message_type ?? "";
570
594
  const timestamp = msg.create_time ? parseInt(msg.create_time, 10) : Date.now();
571
595
  // 历史消息过滤:发送时间超过 5 分钟视为重发,直接丢弃
572
596
  const createTimeMs = msg.create_time ? parseInt(msg.create_time, 10) : NaN;
573
597
  if (!isNaN(createTimeMs) && Date.now() - createTimeMs > STALE_MESSAGE_THRESHOLD_MS) {
574
- console.error(`[feishu] dropped stale message ${messageId} (create_time ${createTimeMs}, age ${Math.round((Date.now() - createTimeMs) / 60000)}min)`);
598
+ this._log("info", `dropped stale message ${messageId}`, {
599
+ create_time: createTimeMs,
600
+ age_min: Math.round((Date.now() - createTimeMs) / 60000),
601
+ });
575
602
  return;
576
603
  }
577
- console.error(`[feishu] received ${messageType} ${chatType} ${chatId} from ${senderId} (content length ${rawContent.length})`);
578
- if (rawContent.length > 0 && rawContent.length <= 500) {
579
- console.error("[feishu] content preview:", rawContent);
580
- }
581
- else if (rawContent.length > 500) {
582
- console.error("[feishu] content preview:", rawContent.slice(0, 500) + "...");
583
- }
604
+ this._log("debug", `received ${messageType} ${chatType} ${chatId} from ${senderId}`, {
605
+ content_len: rawContent.length,
606
+ preview: rawContent.length > 0 ? rawContent.slice(0, 200) : "",
607
+ });
584
608
  // 并行获取用户信息和群聊信息
585
609
  const [userInfo, chatInfo] = await Promise.all([
586
610
  senderId ? this.getUserInfo(senderId) : Promise.resolve(null),
587
611
  chatType === "group" ? this.getChatInfo(chatId) : Promise.resolve(null),
588
612
  ]);
589
- console.error(`[feishu] sender=${senderId} name=${userInfo?.name ?? "(empty)"} chatType=${chatType} chatName=${chatInfo?.name ?? "(none)"}`);
613
+ this._log("debug", `sender=${senderId} name=${userInfo?.name ?? "(empty)"} chatType=${chatType} chatName=${chatInfo?.name ?? "(none)"}`);
590
614
  // 缓存 senderName → senderId,供出站消息 @Name 转换使用
591
615
  if (senderId && userInfo?.name) {
592
616
  this.mentionNameToId.set(userInfo.name, senderId);
@@ -649,6 +673,16 @@ export class FeishuHandler {
649
673
  }
650
674
  }
651
675
  }
676
+ // 忽略机器人自己发送的消息,避免自循环
677
+ if (senderId && this.botOpenId && senderId === this.botOpenId) {
678
+ this._log("debug", `skipped own message ${messageId}`);
679
+ return;
680
+ }
681
+ // chat_id 必须有值才能回复;message_id 不能作为 chat_id 的替代
682
+ if (!chatId) {
683
+ this._log("warn", `dropped message ${messageId}: chat_id 为空,无法路由回复(飞书事件可能异常)`);
684
+ return;
685
+ }
652
686
  // 解析 @mention 占位符(@_user_1 -> @显示名(open_id))
653
687
  const mentions = msg.mentions;
654
688
  const { text: resolvedText, mentionsSelf } = this.resolveMentions(text, mentions);
@@ -657,20 +691,22 @@ export class FeishuHandler {
657
691
  if (msg.parent_id) {
658
692
  text = `[回复消息 ${msg.parent_id}] ${text}`;
659
693
  }
660
- if (chatId || messageId) {
661
- const isP2p = chatType === "p2p";
662
- this.acceptMessage(this.buildBridgeMessage({
663
- chatId: chatId ?? messageId ?? "",
664
- text,
665
- chatType: isP2p ? "私聊" : "群聊",
666
- senderId,
667
- senderName: userInfo?.name || "获取昵称失败",
668
- chatName: chatInfo?.name,
669
- mentionsSelf,
670
- attachments,
671
- timestamp,
672
- }));
694
+ // 未知消息类型(卡片等)时若文本和附件均空,Aido 会拒绝;提供占位说明
695
+ if (!text.trim() && attachments.length === 0) {
696
+ text = messageType ? `[${messageType} 类型消息,暂不支持解析]` : "[未知类型消息]";
673
697
  }
698
+ const isP2p = chatType === "p2p";
699
+ this.acceptMessage(this.buildBridgeMessage({
700
+ chatId,
701
+ text,
702
+ chatType: isP2p ? "私聊" : "群聊",
703
+ senderId,
704
+ senderName: userInfo?.name || "获取昵称失败",
705
+ chatName: chatInfo?.name,
706
+ mentionsSelf,
707
+ attachments,
708
+ timestamp,
709
+ }));
674
710
  },
675
711
  "im.chat.access_event.bot_p2p_chat_entered_v1": async (data) => {
676
712
  const event = data;
@@ -687,8 +723,11 @@ export class FeishuHandler {
687
723
  "im.chat.member.bot.deleted_v1": async (data) => {
688
724
  const event = data;
689
725
  const chatId = event.chat_id ?? event.event?.chat_id ?? "";
690
- if (chatId && this.onBotRemovedFromGroup)
691
- this.onBotRemovedFromGroup(chatId);
726
+ if (chatId) {
727
+ this._log("info", `bot removed from group ${chatId}`);
728
+ if (this.onBotRemovedFromGroup)
729
+ this.onBotRemovedFromGroup(chatId);
730
+ }
692
731
  },
693
732
  "im.message.message_read_v1": async (data) => {
694
733
  if (this.onMessageRead)
@@ -704,7 +743,7 @@ export class FeishuHandler {
704
743
  const meta = this.msgMetaMap.get(messageId);
705
744
  const chatId = meta?.chatId ?? event.chat_id ?? "";
706
745
  if (!chatId) {
707
- console.error(`[feishu] recalled unknown message ${messageId}, skipping`);
746
+ this._log("warn", `recalled unknown message ${messageId}, skipping`);
708
747
  return;
709
748
  }
710
749
  // 尝试获取被撤回消息的发送者信息
@@ -761,7 +800,7 @@ export class FeishuHandler {
761
800
  const actor = recallLabel[recallType] ?? "某人";
762
801
  text = `[${actor}撤回了一条消息]`;
763
802
  }
764
- console.error(`[feishu] message recalled: ${messageId} by ${msgSenderName || "(unknown)"}(${msgSenderId}) recall_type=${recallType} in ${chatId}`);
803
+ this._log("info", `message recalled: ${messageId} by ${msgSenderName || "(unknown)"}(${msgSenderId}) recall_type=${recallType} in ${chatId}`);
765
804
  const isP2pRecall = chatType === "p2p";
766
805
  this.acceptMessage(this.buildBridgeMessage({
767
806
  chatId,
@@ -782,13 +821,13 @@ export class FeishuHandler {
782
821
  const meta = this.msgMetaMap.get(messageId);
783
822
  const chatId = meta?.chatId ?? "";
784
823
  if (!chatId) {
785
- console.error(`[feishu] reaction on unknown message ${messageId}, skipping`);
824
+ this._log("warn", `reaction on unknown message ${messageId}, skipping`);
786
825
  return;
787
826
  }
788
827
  const reactorInfo = reactorId ? await this.getUserInfo(reactorId) : null;
789
828
  const reactorName = reactorInfo?.name || reactorId || "某人";
790
829
  const chatType = this.inferChatType(chatId);
791
- console.error(`[feishu] reaction ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
830
+ this._log("info", `reaction ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
792
831
  const isP2pReaction = chatType === "p2p";
793
832
  this.acceptMessage(this.buildBridgeMessage({
794
833
  chatId,
@@ -809,13 +848,13 @@ export class FeishuHandler {
809
848
  const meta2 = this.msgMetaMap.get(messageId);
810
849
  const chatId = meta2?.chatId ?? "";
811
850
  if (!chatId) {
812
- console.error(`[feishu] reaction-delete on unknown message ${messageId}, skipping`);
851
+ this._log("warn", `reaction-delete on unknown message ${messageId}, skipping`);
813
852
  return;
814
853
  }
815
854
  const reactorInfo = reactorId ? await this.getUserInfo(reactorId) : null;
816
855
  const reactorName = reactorInfo?.name || reactorId || "某人";
817
856
  const chatType = this.inferChatType(chatId);
818
- console.error(`[feishu] reaction removed ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
857
+ this._log("info", `reaction removed ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
819
858
  const isP2pDel = chatType === "p2p";
820
859
  this.acceptMessage(this.buildBridgeMessage({
821
860
  chatId,
@@ -827,7 +866,7 @@ export class FeishuHandler {
827
866
  }));
828
867
  },
829
868
  });
830
- wsClient.start({ eventDispatcher });
869
+ await wsClientAny.reConnect(true);
831
870
  }
832
871
  /**
833
872
  * 根据文件名/扩展名推断飞书 im.file.create 的 file_type。
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ if (!FEISHU_APP_ID || !FEISHU_APP_SECRET) {
10
10
  process.exit(1);
11
11
  }
12
12
  const handler = new FeishuHandler({ appId: FEISHU_APP_ID, appSecret: FEISHU_APP_SECRET });
13
- handler.setOnBotRemovedFromGroup((chatId) => console.error("[feishu] bot removed from group", chatId));
13
+ handler.setOnBotRemovedFromGroup(() => { });
14
14
  function feishuBuildChannelPrompt() {
15
15
  return `ID 格式:ou_xxx 是飞书用户 ID,oc_xxx 是群组 ID。
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmcl/ailo-mcp-feishu",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Ailo 飞书/Lark 通道 MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,14 +10,20 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "start": "node dist/index.js",
13
- "dev": "tsx src/index.ts"
13
+ "dev": "tsx src/index.ts",
14
+ "postinstall": "patch-package"
14
15
  },
16
+ "files": [
17
+ "dist",
18
+ "patches"
19
+ ],
15
20
  "dependencies": {
16
21
  "@larksuiteoapi/node-sdk": "^1.56.1",
17
- "@lmcl/ailo-mcp-sdk": "^0.1.0",
22
+ "@lmcl/ailo-mcp-sdk": "^0.2.0",
18
23
  "@modelcontextprotocol/sdk": "^1.26.0",
19
24
  "dotenv": "^16.4.5",
20
25
  "form-data": "^4.0.0",
26
+ "patch-package": "^8.0.1",
21
27
  "zod": "^4.3.6"
22
28
  },
23
29
  "devDependencies": {
@@ -0,0 +1,42 @@
1
+ diff --git a/node_modules/@larksuiteoapi/node-sdk/lib/index.js b/node_modules/@larksuiteoapi/node-sdk/lib/index.js
2
+ index e0a1198..efbd5a0 100644
3
+ --- a/node_modules/@larksuiteoapi/node-sdk/lib/index.js
4
+ +++ b/node_modules/@larksuiteoapi/node-sdk/lib/index.js
5
+ @@ -85550,25 +85550,22 @@ class WSClient {
6
+ data: payload
7
+ });
8
+ if (!mergedData) {
9
+ + // 分片未收齐:必须立即 ACK 该帧,否则飞书认为未收到会暂停推送,后续消息延后重试才到
10
+ + const partialRespPayload = { code: HttpStatusCode.ok };
11
+ + this.sendMessage(Object.assign(Object.assign({}, data), { headers: [...data.headers, { key: HeaderKey.biz_rt, value: "0" }], payload: new TextEncoder().encode(JSON.stringify(partialRespPayload)) }));
12
+ return;
13
+ }
14
+ this.logger.debug('[ws]', `receive message, message_type: ${type}; message_id: ${message_id}; trace_id: ${trace_id}; data: ${mergedData.data}`);
15
+ - const respPayload = {
16
+ - code: HttpStatusCode.ok,
17
+ - };
18
+ - const startTime = Date.now();
19
+ - try {
20
+ - const result = yield ((_a = this.eventDispatcher) === null || _a === void 0 ? void 0 : _a.invoke(mergedData, { needCheck: false }));
21
+ - if (result) {
22
+ - respPayload.data = Buffer.from(JSON.stringify(result)).toString("base64");
23
+ - }
24
+ - }
25
+ - catch (error) {
26
+ - respPayload.code = HttpStatusCode.internal_server_error;
27
+ - this.logger.error('[ws]', `invoke event failed, message_type: ${type}; message_id: ${message_id}; trace_id: ${trace_id}; error: ${error}`);
28
+ + // [patch] 立即 ACK,不等 handler 完成。飞书要求 3 秒内 ACK,handler 可能耗时远超此限。
29
+ + const immediateAckPayload = { code: HttpStatusCode.ok };
30
+ + this.sendMessage(Object.assign(Object.assign({}, data), { headers: [...data.headers, { key: HeaderKey.biz_rt, value: "0" }], payload: new TextEncoder().encode(JSON.stringify(immediateAckPayload)) }));
31
+ + // [patch] handler 异步执行,不阻塞后续消息的 ACK
32
+ + const dispatcher = this.eventDispatcher;
33
+ + if (dispatcher) {
34
+ + dispatcher.invoke(mergedData, { needCheck: false }).catch((error) => {
35
+ + this.logger.error('[ws]', `invoke event failed, message_type: ${type}; message_id: ${message_id}; trace_id: ${trace_id}; error: ${error}`);
36
+ + });
37
+ }
38
+ - const endTime = Date.now();
39
+ - this.sendMessage(Object.assign(Object.assign({}, data), { headers: [...data.headers, { key: HeaderKey.biz_rt, value: String(startTime - endTime) }], payload: new TextEncoder().encode(JSON.stringify(respPayload)) }));
40
+ });
41
+ }
42
+ sendMessage(data) {