@lmcl/ailo-mcp-feishu 0.0.8 → 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 +40 -43
- package/dist/index.js +2 -0
- package/package.json +2 -2
- package/src/feishu-handler.ts +46 -55
- package/src/index.ts +2 -0
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;
|
|
@@ -309,30 +302,30 @@ export class FeishuHandler {
|
|
|
309
302
|
this.onMessageRead = handler;
|
|
310
303
|
}
|
|
311
304
|
buildBridgeMessage(opts) {
|
|
312
|
-
const { chatId, text, chatType, senderId = "", senderName = "", chatName,
|
|
305
|
+
const { chatId, text, chatType, senderId = "", senderName = "", chatName, attachments } = opts;
|
|
313
306
|
const isPrivate = chatType === "私聊";
|
|
307
|
+
// 新 ContextTag 格式(kind/streamKey/routing):
|
|
308
|
+
// - TagChannel 由网关根据 displayName 自动注入,飞书不重复添加
|
|
309
|
+
// - chat_id 参与 stream_key 推导(streamKey=true),仅路由(routing=true)
|
|
310
|
+
// - 私聊:stream_key = feishu:{ou_xxx}(用户 ID)
|
|
311
|
+
// - 群聊:stream_key = feishu:{oc_xxx}(群 ID)
|
|
312
|
+
// - "@我" 是内容语义不是时空场,不放 contextTags
|
|
313
|
+
// - "时间" 由框架在接收时自动渲染,不由通道传入
|
|
314
314
|
const tags = [
|
|
315
|
-
{
|
|
316
|
-
{
|
|
315
|
+
{ kind: "conv_type", value: chatType, streamKey: false },
|
|
316
|
+
{ kind: "chat_id", value: chatId, streamKey: true, routing: true },
|
|
317
317
|
];
|
|
318
|
-
if (!isPrivate
|
|
319
|
-
|
|
318
|
+
if (!isPrivate) {
|
|
319
|
+
// 群聊:群名作为 stream_key 语义补充,group 标签不参与 stream_key(chat_id 已足够)
|
|
320
|
+
const groupName = chatName || `群${chatId.slice(-8)}`;
|
|
321
|
+
tags.push({ kind: "group", value: groupName, streamKey: false });
|
|
320
322
|
}
|
|
321
|
-
|
|
322
|
-
tags.push({
|
|
323
|
+
if (senderName) {
|
|
324
|
+
tags.push({ kind: "participant", value: senderName, streamKey: false });
|
|
323
325
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
tags.push({
|
|
327
|
-
}
|
|
328
|
-
if (timestamp != null && timestamp > 0) {
|
|
329
|
-
const d = new Date(timestamp);
|
|
330
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
331
|
-
tags.push({
|
|
332
|
-
desc: "时间",
|
|
333
|
-
value: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
|
334
|
-
core: false,
|
|
335
|
-
});
|
|
326
|
+
if (senderId) {
|
|
327
|
+
// sender_id 仅路由备用(recall agent 按参与者查询时使用),不展示在历史邮戳
|
|
328
|
+
tags.push({ kind: "sender_id", value: senderId, streamKey: false, routing: true });
|
|
336
329
|
}
|
|
337
330
|
return { text, contextTags: tags, attachments };
|
|
338
331
|
}
|
|
@@ -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/dist/index.js
CHANGED
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 {
|
|
@@ -381,33 +364,36 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
381
364
|
timestamp?: number;
|
|
382
365
|
attachments?: Array<{ type: string; path?: string; url?: string; base64?: string; mime?: string; name?: string }>;
|
|
383
366
|
}): BridgeMessage {
|
|
384
|
-
const { chatId, text, chatType, senderId = "", senderName = "", chatName,
|
|
367
|
+
const { chatId, text, chatType, senderId = "", senderName = "", chatName, attachments } = opts;
|
|
385
368
|
const isPrivate = chatType === "私聊";
|
|
369
|
+
|
|
370
|
+
// 新 ContextTag 格式(kind/streamKey/routing):
|
|
371
|
+
// - TagChannel 由网关根据 displayName 自动注入,飞书不重复添加
|
|
372
|
+
// - chat_id 参与 stream_key 推导(streamKey=true),仅路由(routing=true)
|
|
373
|
+
// - 私聊:stream_key = feishu:{ou_xxx}(用户 ID)
|
|
374
|
+
// - 群聊:stream_key = feishu:{oc_xxx}(群 ID)
|
|
375
|
+
// - "@我" 是内容语义不是时空场,不放 contextTags
|
|
376
|
+
// - "时间" 由框架在接收时自动渲染,不由通道传入
|
|
386
377
|
const tags: ContextTag[] = [
|
|
387
|
-
{
|
|
388
|
-
{
|
|
378
|
+
{ kind: "conv_type", value: chatType, streamKey: false },
|
|
379
|
+
{ kind: "chat_id", value: chatId, streamKey: true, routing: true },
|
|
389
380
|
];
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
381
|
+
|
|
382
|
+
if (!isPrivate) {
|
|
383
|
+
// 群聊:群名作为 stream_key 语义补充,group 标签不参与 stream_key(chat_id 已足够)
|
|
384
|
+
const groupName = chatName || `群${chatId.slice(-8)}`;
|
|
385
|
+
tags.push({ kind: "group", value: groupName, streamKey: false });
|
|
394
386
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{
|
|
398
|
-
);
|
|
399
|
-
if (mentionsSelf) {
|
|
400
|
-
tags.push({ desc: "@我", value: "是", core: isPrivate });
|
|
387
|
+
|
|
388
|
+
if (senderName) {
|
|
389
|
+
tags.push({ kind: "participant", value: senderName, streamKey: false });
|
|
401
390
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
tags.push({
|
|
406
|
-
desc: "时间",
|
|
407
|
-
value: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`,
|
|
408
|
-
core: false,
|
|
409
|
-
});
|
|
391
|
+
|
|
392
|
+
if (senderId) {
|
|
393
|
+
// sender_id 仅路由备用(recall agent 按参与者查询时使用),不展示在历史邮戳
|
|
394
|
+
tags.push({ kind: "sender_id", value: senderId, streamKey: false, routing: true });
|
|
410
395
|
}
|
|
396
|
+
|
|
411
397
|
return { text, contextTags: tags, attachments };
|
|
412
398
|
}
|
|
413
399
|
|
|
@@ -625,8 +611,9 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
625
611
|
* 使用飞书 WebSocket 长连接接收事件,无需配置回调地址。
|
|
626
612
|
* 需在飞书开放平台「事件与回调」中选择「使用长连接接收事件」并保存(本客户端需在线)。
|
|
627
613
|
*/
|
|
628
|
-
start(): void {
|
|
629
|
-
|
|
614
|
+
start(ctx: ChannelContext): void {
|
|
615
|
+
this.ctx = ctx;
|
|
616
|
+
this.loadExternalUserLabels();
|
|
630
617
|
this.fetchBotOpenId();
|
|
631
618
|
|
|
632
619
|
// MCP stdio 占用 stdout,Lark SDK 的 logger 必须写到 stderr
|
|
@@ -660,7 +647,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
660
647
|
eventDispatcher.register({
|
|
661
648
|
"im.message.receive_v1": async (data: unknown) => {
|
|
662
649
|
const event = data as FeishuMessageEvent;
|
|
663
|
-
if (!event.message || !this.
|
|
650
|
+
if (!event.message || !this.ctx) return;
|
|
664
651
|
|
|
665
652
|
const msg = event.message;
|
|
666
653
|
const chatId = msg.chat_id ?? "";
|
|
@@ -777,7 +764,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
777
764
|
|
|
778
765
|
if (chatId || messageId) {
|
|
779
766
|
const isP2p = chatType === "p2p";
|
|
780
|
-
this.
|
|
767
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
781
768
|
chatId: chatId ?? messageId ?? "",
|
|
782
769
|
text,
|
|
783
770
|
chatType: isP2p ? "私聊" : "群聊",
|
|
@@ -809,7 +796,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
809
796
|
if (this.onMessageRead) this.onMessageRead(data);
|
|
810
797
|
},
|
|
811
798
|
"im.message.recalled_v1": async (data: unknown) => {
|
|
812
|
-
if (!this.
|
|
799
|
+
if (!this.ctx) return;
|
|
813
800
|
const event = data as FeishuRecalledEvent;
|
|
814
801
|
const messageId = event.message_id ?? "";
|
|
815
802
|
const recallType = event.recall_type ?? "unknown";
|
|
@@ -877,7 +864,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
877
864
|
console.error(`[feishu] message recalled: ${messageId} by ${msgSenderName || "(unknown)"}(${msgSenderId}) recall_type=${recallType} in ${chatId}`);
|
|
878
865
|
|
|
879
866
|
const isP2pRecall = chatType === "p2p";
|
|
880
|
-
this.
|
|
867
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
881
868
|
chatId,
|
|
882
869
|
text,
|
|
883
870
|
chatType: isP2pRecall ? "私聊" : "群聊",
|
|
@@ -887,7 +874,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
887
874
|
}));
|
|
888
875
|
},
|
|
889
876
|
"im.message.reaction.created_v1": async (data: unknown) => {
|
|
890
|
-
if (!this.
|
|
877
|
+
if (!this.ctx) return;
|
|
891
878
|
const event = data as FeishuReactionEvent;
|
|
892
879
|
const messageId = event.message_id ?? "";
|
|
893
880
|
const emoji = event.reaction_type?.emoji_type ?? "UNKNOWN";
|
|
@@ -907,7 +894,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
907
894
|
console.error(`[feishu] reaction ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
|
|
908
895
|
|
|
909
896
|
const isP2pReaction = chatType === "p2p";
|
|
910
|
-
this.
|
|
897
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
911
898
|
chatId,
|
|
912
899
|
text: `[${reactorName} 对一条消息贴了表情 ${emoji} (message_id: ${messageId})]`,
|
|
913
900
|
chatType: isP2pReaction ? "私聊" : "群聊",
|
|
@@ -917,7 +904,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
917
904
|
}));
|
|
918
905
|
},
|
|
919
906
|
"im.message.reaction.deleted_v1": async (data: unknown) => {
|
|
920
|
-
if (!this.
|
|
907
|
+
if (!this.ctx) return;
|
|
921
908
|
const event = data as FeishuReactionEvent;
|
|
922
909
|
const messageId = event.message_id ?? "";
|
|
923
910
|
const emoji = event.reaction_type?.emoji_type ?? "UNKNOWN";
|
|
@@ -937,7 +924,7 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
937
924
|
console.error(`[feishu] reaction removed ${emoji} on ${messageId} by ${reactorName}(${reactorId})`);
|
|
938
925
|
|
|
939
926
|
const isP2pDel = chatType === "p2p";
|
|
940
|
-
this.
|
|
927
|
+
this.acceptMessage(this.buildBridgeMessage({
|
|
941
928
|
chatId,
|
|
942
929
|
text: `[${reactorName} 移除了表情 ${emoji} (message_id: ${messageId})]`,
|
|
943
930
|
chatType: isP2pDel ? "私聊" : "群聊",
|
|
@@ -1183,6 +1170,10 @@ export class FeishuHandler implements BridgeHandler {
|
|
|
1183
1170
|
}
|
|
1184
1171
|
}
|
|
1185
1172
|
|
|
1173
|
+
async stop(): Promise<void> {
|
|
1174
|
+
this.ctx = null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1186
1177
|
/** 处理来自 MCP 工具 feishu action=set_nickname 的命令 */
|
|
1187
1178
|
async onCommand(command: string, params: Record<string, unknown>): Promise<void> {
|
|
1188
1179
|
if (command !== "set_nickname") return;
|