@lmcl/ailo-mcp-feishu 0.0.9 → 0.1.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 +21 -24
- package/package.json +2 -2
- package/src/feishu-handler.ts +22 -34
package/dist/feishu-handler.js
CHANGED
|
@@ -9,22 +9,17 @@ export class FeishuHandler {
|
|
|
9
9
|
config;
|
|
10
10
|
client;
|
|
11
11
|
wsClient = null;
|
|
12
|
-
|
|
12
|
+
ctx = null;
|
|
13
13
|
onP2PChatEntered = null;
|
|
14
14
|
onBotAddedToGroup = null;
|
|
15
15
|
onBotRemovedFromGroup = null;
|
|
16
16
|
onMessageRead = null;
|
|
17
|
-
// bot 自身的 open_id,用于检测 mentionsSelf
|
|
18
17
|
botOpenId = "";
|
|
19
|
-
// @提及名称 → open_id 缓存,用于将出站文本中的 @Name 转为飞书 <at> 标签
|
|
20
18
|
mentionNameToId = new Map();
|
|
21
|
-
// 懒加载缓存
|
|
22
19
|
userCache = new Map();
|
|
23
20
|
chatCache = new Map();
|
|
24
|
-
// 外部用户顺序编号(userId → 编号标签),通过 ChannelStorage 持久化,跨重启保持稳定
|
|
25
21
|
externalUserCounter = 0;
|
|
26
22
|
externalUserLabels = new Map();
|
|
27
|
-
storage = null;
|
|
28
23
|
// messageId → 消息元数据映射缓存(供撤回/Reaction 等事件查找)
|
|
29
24
|
msgMetaMap = new Map();
|
|
30
25
|
MSG_META_MAP_MAX = 2000;
|
|
@@ -42,12 +37,8 @@ export class FeishuHandler {
|
|
|
42
37
|
loggerLevel: Lark.LoggerLevel.fatal,
|
|
43
38
|
});
|
|
44
39
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*/
|
|
48
|
-
setDataProvider(provider) {
|
|
49
|
-
this.storage = provider;
|
|
50
|
-
this.loadExternalUserLabels();
|
|
40
|
+
get storage() {
|
|
41
|
+
return this.ctx?.storage ?? null;
|
|
51
42
|
}
|
|
52
43
|
/** 外部用户数据单 key:{ _counter, [userId]: label } */
|
|
53
44
|
static EXTERNAL_USERS_KEY = "external_users";
|
|
@@ -293,8 +284,10 @@ export class FeishuHandler {
|
|
|
293
284
|
}
|
|
294
285
|
return { text: resolved, mentionsSelf };
|
|
295
286
|
}
|
|
296
|
-
|
|
297
|
-
this.
|
|
287
|
+
acceptMessage(msg) {
|
|
288
|
+
if (!this.ctx)
|
|
289
|
+
return;
|
|
290
|
+
this.ctx.accept(msg).catch((err) => console.error("[feishu] accept failed:", err));
|
|
298
291
|
}
|
|
299
292
|
setOnP2PChatEntered(handler) {
|
|
300
293
|
this.onP2PChatEntered = handler;
|
|
@@ -533,8 +526,9 @@ export class FeishuHandler {
|
|
|
533
526
|
* 使用飞书 WebSocket 长连接接收事件,无需配置回调地址。
|
|
534
527
|
* 需在飞书开放平台「事件与回调」中选择「使用长连接接收事件」并保存(本客户端需在线)。
|
|
535
528
|
*/
|
|
536
|
-
start() {
|
|
537
|
-
|
|
529
|
+
start(ctx) {
|
|
530
|
+
this.ctx = ctx;
|
|
531
|
+
this.loadExternalUserLabels();
|
|
538
532
|
this.fetchBotOpenId();
|
|
539
533
|
// MCP stdio 占用 stdout,Lark SDK 的 logger 必须写到 stderr
|
|
540
534
|
const stderrLogger = {
|
|
@@ -561,7 +555,7 @@ export class FeishuHandler {
|
|
|
561
555
|
eventDispatcher.register({
|
|
562
556
|
"im.message.receive_v1": async (data) => {
|
|
563
557
|
const event = data;
|
|
564
|
-
if (!event.message || !this.
|
|
558
|
+
if (!event.message || !this.ctx)
|
|
565
559
|
return;
|
|
566
560
|
const msg = event.message;
|
|
567
561
|
const chatId = msg.chat_id ?? "";
|
|
@@ -665,7 +659,7 @@ export class FeishuHandler {
|
|
|
665
659
|
}
|
|
666
660
|
if (chatId || messageId) {
|
|
667
661
|
const isP2p = chatType === "p2p";
|
|
668
|
-
this.
|
|
662
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
669
663
|
chatId: chatId ?? messageId ?? "",
|
|
670
664
|
text,
|
|
671
665
|
chatType: isP2p ? "私聊" : "群聊",
|
|
@@ -701,7 +695,7 @@ export class FeishuHandler {
|
|
|
701
695
|
this.onMessageRead(data);
|
|
702
696
|
},
|
|
703
697
|
"im.message.recalled_v1": async (data) => {
|
|
704
|
-
if (!this.
|
|
698
|
+
if (!this.ctx)
|
|
705
699
|
return;
|
|
706
700
|
const event = data;
|
|
707
701
|
const messageId = event.message_id ?? "";
|
|
@@ -769,7 +763,7 @@ export class FeishuHandler {
|
|
|
769
763
|
}
|
|
770
764
|
console.error(`[feishu] message recalled: ${messageId} by ${msgSenderName || "(unknown)"}(${msgSenderId}) recall_type=${recallType} in ${chatId}`);
|
|
771
765
|
const isP2pRecall = chatType === "p2p";
|
|
772
|
-
this.
|
|
766
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
773
767
|
chatId,
|
|
774
768
|
text,
|
|
775
769
|
chatType: isP2pRecall ? "私聊" : "群聊",
|
|
@@ -779,7 +773,7 @@ export class FeishuHandler {
|
|
|
779
773
|
}));
|
|
780
774
|
},
|
|
781
775
|
"im.message.reaction.created_v1": async (data) => {
|
|
782
|
-
if (!this.
|
|
776
|
+
if (!this.ctx)
|
|
783
777
|
return;
|
|
784
778
|
const event = data;
|
|
785
779
|
const messageId = event.message_id ?? "";
|
|
@@ -796,7 +790,7 @@ export class FeishuHandler {
|
|
|
796
790
|
const chatType = this.inferChatType(chatId);
|
|
797
791
|
console.error(`[feishu] reaction ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
|
|
798
792
|
const isP2pReaction = chatType === "p2p";
|
|
799
|
-
this.
|
|
793
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
800
794
|
chatId,
|
|
801
795
|
text: `[${reactorName} 对一条消息贴了表情 ${emoji} (message_id: ${messageId})]`,
|
|
802
796
|
chatType: isP2pReaction ? "私聊" : "群聊",
|
|
@@ -806,7 +800,7 @@ export class FeishuHandler {
|
|
|
806
800
|
}));
|
|
807
801
|
},
|
|
808
802
|
"im.message.reaction.deleted_v1": async (data) => {
|
|
809
|
-
if (!this.
|
|
803
|
+
if (!this.ctx)
|
|
810
804
|
return;
|
|
811
805
|
const event = data;
|
|
812
806
|
const messageId = event.message_id ?? "";
|
|
@@ -823,7 +817,7 @@ export class FeishuHandler {
|
|
|
823
817
|
const chatType = this.inferChatType(chatId);
|
|
824
818
|
console.error(`[feishu] reaction removed ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
|
|
825
819
|
const isP2pDel = chatType === "p2p";
|
|
826
|
-
this.
|
|
820
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
827
821
|
chatId,
|
|
828
822
|
text: `[${reactorName} 移除了表情 ${emoji} (message_id: ${messageId})]`,
|
|
829
823
|
chatType: isP2pDel ? "私聊" : "群聊",
|
|
@@ -1047,6 +1041,9 @@ export class FeishuHandler {
|
|
|
1047
1041
|
});
|
|
1048
1042
|
}
|
|
1049
1043
|
}
|
|
1044
|
+
async stop() {
|
|
1045
|
+
this.ctx = null;
|
|
1046
|
+
}
|
|
1050
1047
|
/** 处理来自 MCP 工具 feishu action=set_nickname 的命令 */
|
|
1051
1048
|
async onCommand(command, params) {
|
|
1052
1049
|
if (command !== "set_nickname")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmcl/ailo-mcp-feishu",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Ailo 飞书/Lark 通道 MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@larksuiteoapi/node-sdk": "^1.56.1",
|
|
17
|
-
"@lmcl/ailo-mcp-sdk": "^0.0
|
|
17
|
+
"@lmcl/ailo-mcp-sdk": "^0.1.0",
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
19
19
|
"dotenv": "^16.4.5",
|
|
20
20
|
"form-data": "^4.0.0",
|
package/src/feishu-handler.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
|
-
import { getWorkDir, type BridgeHandler, type BridgeMessage, type ContextTag } from "@lmcl/ailo-mcp-sdk";
|
|
5
|
+
import { getWorkDir, type BridgeHandler, type BridgeMessage, type ChannelContext, type ChannelStorage, type ContextTag } from "@lmcl/ailo-mcp-sdk";
|
|
6
6
|
import {
|
|
7
7
|
type CacheEntry,
|
|
8
8
|
type ChatInfo,
|
|
@@ -29,35 +29,21 @@ import {
|
|
|
29
29
|
|
|
30
30
|
export type { FeishuConfig, FeishuAttachment, OnChatId } from "./feishu-types.js";
|
|
31
31
|
|
|
32
|
-
type ChannelStorage = {
|
|
33
|
-
getData(key: string): Promise<string | null>;
|
|
34
|
-
setData(key: string, value: string): Promise<void>;
|
|
35
|
-
deleteData(key: string): Promise<void>;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
32
|
export class FeishuHandler implements BridgeHandler {
|
|
39
33
|
private client: Lark.Client;
|
|
40
34
|
private wsClient: Lark.WSClient | null = null;
|
|
41
|
-
private
|
|
35
|
+
private ctx: ChannelContext | null = null;
|
|
42
36
|
private onP2PChatEntered: OnChatId | null = null;
|
|
43
37
|
private onBotAddedToGroup: OnChatId | null = null;
|
|
44
38
|
private onBotRemovedFromGroup: OnChatId | null = null;
|
|
45
39
|
private onMessageRead: ((data: unknown) => void) | null = null;
|
|
46
40
|
|
|
47
|
-
// bot 自身的 open_id,用于检测 mentionsSelf
|
|
48
41
|
private botOpenId: string = "";
|
|
49
|
-
|
|
50
|
-
// @提及名称 → open_id 缓存,用于将出站文本中的 @Name 转为飞书 <at> 标签
|
|
51
42
|
private mentionNameToId = new Map<string, string>();
|
|
52
|
-
|
|
53
|
-
// 懒加载缓存
|
|
54
43
|
private userCache = new Map<string, CacheEntry<UserInfo>>();
|
|
55
44
|
private chatCache = new Map<string, CacheEntry<ChatInfo>>();
|
|
56
|
-
|
|
57
|
-
// 外部用户顺序编号(userId → 编号标签),通过 ChannelStorage 持久化,跨重启保持稳定
|
|
58
45
|
private externalUserCounter = 0;
|
|
59
46
|
private externalUserLabels = new Map<string, string>();
|
|
60
|
-
private storage: ChannelStorage | null = null;
|
|
61
47
|
|
|
62
48
|
// messageId → 消息元数据映射缓存(供撤回/Reaction 等事件查找)
|
|
63
49
|
private msgMetaMap = new Map<string, { chatId: string; senderId: string; senderName: string }>();
|
|
@@ -79,12 +65,8 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
79
65
|
|
|
80
66
|
}
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*/
|
|
85
|
-
setDataProvider?(provider: ChannelStorage): void {
|
|
86
|
-
this.storage = provider;
|
|
87
|
-
this.loadExternalUserLabels();
|
|
68
|
+
private get storage(): ChannelStorage | null {
|
|
69
|
+
return this.ctx?.storage ?? null;
|
|
88
70
|
}
|
|
89
71
|
|
|
90
72
|
/** 外部用户数据单 key:{ _counter, [userId]: label } */
|
|
@@ -350,8 +332,9 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
350
332
|
return { text: resolved, mentionsSelf };
|
|
351
333
|
}
|
|
352
334
|
|
|
353
|
-
|
|
354
|
-
this.
|
|
335
|
+
private acceptMessage(msg: BridgeMessage): void {
|
|
336
|
+
if (!this.ctx) return;
|
|
337
|
+
this.ctx.accept(msg).catch((err) => console.error("[feishu] accept failed:", err));
|
|
355
338
|
}
|
|
356
339
|
|
|
357
340
|
setOnP2PChatEntered(handler: OnChatId | null): void {
|
|
@@ -628,8 +611,9 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
628
611
|
* 使用飞书 WebSocket 长连接接收事件,无需配置回调地址。
|
|
629
612
|
* 需在飞书开放平台「事件与回调」中选择「使用长连接接收事件」并保存(本客户端需在线)。
|
|
630
613
|
*/
|
|
631
|
-
start(): void {
|
|
632
|
-
|
|
614
|
+
start(ctx: ChannelContext): void {
|
|
615
|
+
this.ctx = ctx;
|
|
616
|
+
this.loadExternalUserLabels();
|
|
633
617
|
this.fetchBotOpenId();
|
|
634
618
|
|
|
635
619
|
// MCP stdio 占用 stdout,Lark SDK 的 logger 必须写到 stderr
|
|
@@ -663,7 +647,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
663
647
|
eventDispatcher.register({
|
|
664
648
|
"im.message.receive_v1": async (data: unknown) => {
|
|
665
649
|
const event = data as FeishuMessageEvent;
|
|
666
|
-
if (!event.message || !this.
|
|
650
|
+
if (!event.message || !this.ctx) return;
|
|
667
651
|
|
|
668
652
|
const msg = event.message;
|
|
669
653
|
const chatId = msg.chat_id ?? "";
|
|
@@ -780,7 +764,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
780
764
|
|
|
781
765
|
if (chatId || messageId) {
|
|
782
766
|
const isP2p = chatType === "p2p";
|
|
783
|
-
this.
|
|
767
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
784
768
|
chatId: chatId ?? messageId ?? "",
|
|
785
769
|
text,
|
|
786
770
|
chatType: isP2p ? "私聊" : "群聊",
|
|
@@ -812,7 +796,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
812
796
|
if (this.onMessageRead) this.onMessageRead(data);
|
|
813
797
|
},
|
|
814
798
|
"im.message.recalled_v1": async (data: unknown) => {
|
|
815
|
-
if (!this.
|
|
799
|
+
if (!this.ctx) return;
|
|
816
800
|
const event = data as FeishuRecalledEvent;
|
|
817
801
|
const messageId = event.message_id ?? "";
|
|
818
802
|
const recallType = event.recall_type ?? "unknown";
|
|
@@ -880,7 +864,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
880
864
|
console.error(`[feishu] message recalled: ${messageId} by ${msgSenderName || "(unknown)"}(${msgSenderId}) recall_type=${recallType} in ${chatId}`);
|
|
881
865
|
|
|
882
866
|
const isP2pRecall = chatType === "p2p";
|
|
883
|
-
this.
|
|
867
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
884
868
|
chatId,
|
|
885
869
|
text,
|
|
886
870
|
chatType: isP2pRecall ? "私聊" : "群聊",
|
|
@@ -890,7 +874,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
890
874
|
}));
|
|
891
875
|
},
|
|
892
876
|
"im.message.reaction.created_v1": async (data: unknown) => {
|
|
893
|
-
if (!this.
|
|
877
|
+
if (!this.ctx) return;
|
|
894
878
|
const event = data as FeishuReactionEvent;
|
|
895
879
|
const messageId = event.message_id ?? "";
|
|
896
880
|
const emoji = event.reaction_type?.emoji_type ?? "UNKNOWN";
|
|
@@ -910,7 +894,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
910
894
|
console.error(`[feishu] reaction ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
|
|
911
895
|
|
|
912
896
|
const isP2pReaction = chatType === "p2p";
|
|
913
|
-
this.
|
|
897
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
914
898
|
chatId,
|
|
915
899
|
text: `[${reactorName} 对一条消息贴了表情 ${emoji} (message_id: ${messageId})]`,
|
|
916
900
|
chatType: isP2pReaction ? "私聊" : "群聊",
|
|
@@ -920,7 +904,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
920
904
|
}));
|
|
921
905
|
},
|
|
922
906
|
"im.message.reaction.deleted_v1": async (data: unknown) => {
|
|
923
|
-
if (!this.
|
|
907
|
+
if (!this.ctx) return;
|
|
924
908
|
const event = data as FeishuReactionEvent;
|
|
925
909
|
const messageId = event.message_id ?? "";
|
|
926
910
|
const emoji = event.reaction_type?.emoji_type ?? "UNKNOWN";
|
|
@@ -940,7 +924,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
940
924
|
console.error(`[feishu] reaction removed ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
|
|
941
925
|
|
|
942
926
|
const isP2pDel = chatType === "p2p";
|
|
943
|
-
this.
|
|
927
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
944
928
|
chatId,
|
|
945
929
|
text: `[${reactorName} 移除了表情 ${emoji} (message_id: ${messageId})]`,
|
|
946
930
|
chatType: isP2pDel ? "私聊" : "群聊",
|
|
@@ -1186,6 +1170,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1186
1170
|
}
|
|
1187
1171
|
}
|
|
1188
1172
|
|
|
1173
|
+
async stop(): Promise<void> {
|
|
1174
|
+
this.ctx = null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1189
1177
|
/** 处理来自 MCP 工具 feishu action=set_nickname 的命令 */
|
|
1190
1178
|
async onCommand(command: string, params: Record<string, unknown>): Promise<void> {
|
|
1191
1179
|
if (command !== "set_nickname") return;
|