@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.
- package/dist/feishu-handler.js +101 -62
- package/dist/index.js +1 -1
- package/package.json +9 -3
- package/patches/@larksuiteoapi+node-sdk+1.59.0.patch +42 -0
- package/src/feishu-handler.ts +0 -1189
- package/src/feishu-types.ts +0 -86
- package/src/feishu-utils.ts +0 -165
- package/src/index.ts +0 -39
- package/src/mcp-server.ts +0 -64
- package/tsconfig.json +0 -13
package/dist/feishu-handler.js
CHANGED
|
@@ -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
|
-
|
|
74
|
+
this._log("info", `已加载外部用户映射: ${this.externalUserLabels.size} 条, counter=${this.externalUserCounter}`);
|
|
65
75
|
}
|
|
66
76
|
catch {
|
|
67
|
-
|
|
77
|
+
this._log("warn", "解析外部用户数据失败,将从零开始");
|
|
68
78
|
}
|
|
69
79
|
})
|
|
70
80
|
.catch((err) => {
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
this._log("warn", "fetchDocWithImageRefs docs failed for both APIs");
|
|
136
146
|
return null;
|
|
137
147
|
}
|
|
138
|
-
|
|
148
|
+
this._log("warn", "fetchDocWithImageRefs: unsupported doc type", { type });
|
|
139
149
|
return null;
|
|
140
150
|
}
|
|
141
151
|
catch (err) {
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
+
this._log("info", `bot open_id: ${this.botOpenId}`);
|
|
257
267
|
}
|
|
258
268
|
// 获取失败时仅 debug 级别,不影响核心功能(仅群聊 @检测 会失效)
|
|
259
269
|
}
|
|
260
270
|
catch (err) {
|
|
261
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
403
|
+
this._log("info", `getUserInfo(${userId}): 外部用户,无权限获取通讯录信息 (41050)`);
|
|
394
404
|
}
|
|
395
405
|
else {
|
|
396
406
|
const detail = err?.response?.data ?? err.message;
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: (
|
|
536
|
-
warn: (
|
|
537
|
-
info: (
|
|
538
|
-
debug: (
|
|
539
|
-
trace: (
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
691
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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.
|
|
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.
|
|
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) {
|