@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.
@@ -9,22 +9,17 @@ export class FeishuHandler {
9
9
  config;
10
10
  client;
11
11
  wsClient = null;
12
- onMessageHandler = null;
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
- * 注入持久化存储(SDK 连接后注入,含 getData/setData/deleteData)。
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
- setOnMessage(handler) {
297
- this.onMessageHandler = handler;
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
- // 获取 bot 自身的 open_id(异步,不阻塞启动)
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.onMessageHandler)
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.onMessageHandler(this.buildBridgeMessage({
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.onMessageHandler)
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.onMessageHandler(this.buildBridgeMessage({
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.onMessageHandler)
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.onMessageHandler(this.buildBridgeMessage({
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.onMessageHandler)
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.onMessageHandler(this.buildBridgeMessage({
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.9",
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.4",
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",
@@ -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 onMessageHandler: ((msg: BridgeMessage) => void | Promise<void>) | null = null;
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
- * 注入持久化存储(SDK 连接后注入,含 getData/setData/deleteData)。
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
- setOnMessage(handler: (msg: BridgeMessage) => void | Promise<void>): void {
354
- this.onMessageHandler = handler;
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
- // 获取 bot 自身的 open_id(异步,不阻塞启动)
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.onMessageHandler) return;
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.onMessageHandler!(this.buildBridgeMessage({
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.onMessageHandler) return;
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.onMessageHandler!(this.buildBridgeMessage({
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.onMessageHandler) return;
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.onMessageHandler!(this.buildBridgeMessage({
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.onMessageHandler) return;
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.onMessageHandler!(this.buildBridgeMessage({
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;